cistack 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,12 +10,15 @@
10
10
 
11
11
  - 🔍 **Deep codebase analysis** — reads `package.json`, lock files, config files, and directory structure
12
12
  - 🧠 **Smart detection** — identifies 30+ frameworks, 12 languages, 12+ testing tools, and 10+ hosting platforms
13
+ - ⚡ **Native Cache support** — speeds up pipelines by 2–4min using native caching for npm, pip, go, cargo, maven, gradle, and bundler
14
+ - ✨ **PR Preview Deploys** — automatic preview environments for Vercel and Netlify on every pull request
13
15
  - 🚀 **Hosting auto-detect** — Firebase, Vercel, Netlify, AWS, GCP, Azure, Heroku, Render, Railway, GitHub Pages, Docker
14
- - 🏗️ **Multi-workflow output** — generates separate `ci.yml`, `deploy.yml`, `docker.yml`, and `security.yml` as appropriate
16
+ - 🛡️ **Workflow Audit & Upgrade** — analyse existing `.github/workflows` for outdated actions and missing best practices
17
+ - 🏗️ **Multi-workflow output** — generates separate `ci.yml`, `deploy.yml`, `docker.yml`, and `security.yml`
15
18
  - 🔒 **Security built-in** — CodeQL analysis + dependency auditing on every pipeline
16
- - 📦 **Monorepo aware** — detects Turborepo, Nx, Lerna, pnpm workspaces
19
+ - 📦 **Monorepo aware** — detects Turborepo, Nx, Lerna, pnpm workspaces (supports per-package workflows)
17
20
  - ✅ **Interactive mode** — confirms detected settings before writing files
18
- - 🎯 **Zero config** — works out of the box with no configuration needed
21
+ - 🎯 **Zero config** — works out of the box with `cistack.config.js` for overrides
19
22
 
20
23
  ---
21
24
 
@@ -33,10 +36,15 @@ npm install -g cistack
33
36
 
34
37
  ## Usage
35
38
 
39
+ ### Generate Pipelines
40
+ Analyze your stack and generate best-practice workflows.
36
41
  ```bash
37
42
  # In your project directory
38
43
  npx cistack
39
44
 
45
+ # Show reasoning for detected stack
46
+ npx cistack --explain
47
+
40
48
  # Specify a project path
41
49
  npx cistack --path /path/to/project
42
50
 
@@ -45,19 +53,40 @@ npx cistack --output .github/workflows
45
53
 
46
54
  # Dry run (print YAML without writing files)
47
55
  npx cistack --dry-run
56
+ ```
48
57
 
49
- # Skip interactive prompts
50
- npx cistack --no-prompt
58
+ ### Audit Existing Workflows
59
+ Analyze your current `.github/workflows` folder for outdated actions or missing features.
60
+ ```bash
61
+ npx cistack audit
62
+ ```
51
63
 
52
- # Verbose output
53
- npx cistack --verbose
64
+ ### Automatic Upgrade
65
+ Automatically bump all action versions (e.g., `actions/checkout@v3` → `@v4`) across all your workflow files to the latest stable releases.
66
+ ```bash
67
+ npx cistack upgrade
68
+ ```
54
69
 
55
- # Force overwrite existing files
56
- npx cistack --force
70
+ ### Initialization
71
+ Create a `cistack.config.js` to override auto-detected settings.
72
+ ```bash
73
+ npx cistack init
57
74
  ```
58
75
 
59
76
  ---
60
77
 
78
+ ## Flags
79
+
80
+ - `--explain` — Show detailed reasoning for every detection (build trust)
81
+ - `--dry-run` — Print YAML to terminal without writing to disk
82
+ - `--force` — Overwrite existing files instead of smart-merging
83
+ - `--no-prompt` — Skip interactive confirmation
84
+ - `--verbose` — Show raw analysis data
85
+ - `--path <dir>` — Project root directory
86
+ - `--output <dir>` — Workflow output directory (default: `.github/workflows`)
87
+
88
+ ---
89
+
61
90
  ## Detected Hosting Platforms
62
91
 
63
92
  | Platform | Detection Signal |
@@ -112,21 +141,23 @@ Runs on every push and pull request:
112
141
  2. **Test** — unit tests with coverage upload (matrix across Node versions)
113
142
  3. **Build** — production build, artifact upload
114
143
  4. **E2E** — Cypress / Playwright (if detected)
144
+ 5. **Caching** — Full dependency caching for faster runs
115
145
 
116
146
  ### `deploy.yml` — Continuous Deployment
117
147
  Triggers on push to `main`/`master` + manual dispatch:
118
- - Platform-specific deploy using the best available GitHub Action
148
+ - Platform-specific deploy using official GitHub Actions
149
+ - **PR Preview Deploys** — automatic previews for Vercel and Netlify pull requests
119
150
  - Proper secret references documented in the file header
120
151
 
121
152
  ### `docker.yml` — Docker Build & Push
122
153
  Triggers on push to `main` and version tags:
123
154
  - Multi-platform build via Docker Buildx
124
155
  - Pushes to GitHub Container Registry (GHCR)
125
- - Build cache via GitHub Actions cache
156
+ - Build cache via GitHub Actions cache (GHA)
126
157
 
127
158
  ### `security.yml` — Security Audit
128
159
  Runs on push, PRs, and weekly schedule:
129
- - Dependency vulnerability audit (npm audit / safety / etc.)
160
+ - Dependency vulnerability audit (npm audit / safety / cargo audit)
130
161
  - GitHub CodeQL analysis for the detected language
131
162
 
132
163
  ---
@@ -142,28 +173,11 @@ Each generated `deploy.yml` has a comment at the top listing the exact secrets n
142
173
 
143
174
  ## Examples
144
175
 
145
- **Next.js + Vercel project:**
146
- ```
147
- npx cistack
148
- # .github/workflows/ci.yml (lint, test, build)
149
- # .github/workflows/deploy.yml (vercel deploy)
150
- # → .github/workflows/security.yml
151
- ```
152
-
153
- **Firebase + React project:**
154
- ```
155
- npx cistack
156
- # → .github/workflows/ci.yml
157
- # → .github/workflows/deploy.yml (firebase deploy --only hosting)
158
- # → .github/workflows/security.yml
159
- ```
160
-
161
- **Node.js API + Docker:**
162
- ```
163
- npx cistack
164
- # → .github/workflows/ci.yml
165
- # → .github/workflows/docker.yml (GHCR push)
166
- # → .github/workflows/security.yml
176
+ **Next.js + Vercel project with Audit:**
177
+ ```bash
178
+ npx cistack audit # Check existing workflows
179
+ npx cistack upgrade # Update versions to v4
180
+ npx cistack generate # Refresh with latest caching & previews
167
181
  ```
168
182
 
169
183
  ---
package/bin/ciflow.js CHANGED
@@ -9,7 +9,7 @@ const CIFlow = require('../src/index');
9
9
  program
10
10
  .name('cistack')
11
11
  .description('Generate GitHub Actions CI/CD pipelines by analysing your codebase')
12
- .version('1.0.0');
12
+ .version('2.0.0');
13
13
 
14
14
  program
15
15
  .command('generate', { isDefault: true })
@@ -17,19 +17,103 @@ program
17
17
  .option('-p, --path <dir>', 'Path to the project root', process.cwd())
18
18
  .option('-o, --output <dir>', 'Output directory for workflow files', '.github/workflows')
19
19
  .option('--dry-run', 'Print the generated YAML without writing files')
20
- .option('--force', 'Overwrite existing workflow files without prompting')
20
+ .option('--force', 'Overwrite existing workflow files without smart-merge')
21
21
  .option('--no-prompt', 'Skip interactive prompts and use detected settings')
22
22
  .option('--verbose', 'Show detailed analysis output')
23
+ .option('--explain', 'Show reasoning for detected stack')
23
24
  .action(async (options) => {
24
25
  const ciflow = new CIFlow({
25
26
  projectPath: path.resolve(options.path),
26
- outputDir: options.output,
27
- dryRun: options.dryRun,
28
- force: options.force,
29
- prompt: options.prompt,
30
- verbose: options.verbose,
27
+ outputDir: options.output,
28
+ dryRun: options.dryRun,
29
+ force: options.force,
30
+ prompt: options.prompt,
31
+ verbose: options.verbose,
32
+ explain: options.explain,
31
33
  });
32
34
  await ciflow.run();
33
35
  });
34
36
 
37
+ program
38
+ .command('audit')
39
+ .description("Analyse existing .github/workflows/ folder and suggest fixes")
40
+ .option('-p, --path <dir>', 'Path to the project root', process.cwd())
41
+ .action(async (options) => {
42
+ const ciflow = new CIFlow({ projectPath: path.resolve(options.path) });
43
+ await ciflow.audit();
44
+ });
45
+
46
+ program
47
+ .command('upgrade')
48
+ .description("Automatically bump action versions across all workflow files")
49
+ .option('-p, --path <dir>', 'Path to the project root', process.cwd())
50
+ .option('--dry-run', 'Show what would be upgraded without modifying files')
51
+ .action(async (options) => {
52
+ const ciflow = new CIFlow({
53
+ projectPath: path.resolve(options.path),
54
+ dryRun: options.dryRun
55
+ });
56
+ await ciflow.upgrade();
57
+ });
58
+
59
+ program
60
+ .command('init')
61
+ // ... rest of init
62
+ .description('Create a starter cistack.config.js in the current directory')
63
+ .option('-p, --path <dir>', 'Path to the project root', process.cwd())
64
+ .action(async (options) => {
65
+ const fs = require('fs');
66
+ const resolvedPath = path.resolve(options.path);
67
+ const configPath = path.join(resolvedPath, 'cistack.config.js');
68
+
69
+ // Ensure the target directory exists
70
+ if (!fs.existsSync(resolvedPath)) {
71
+ fs.mkdirSync(resolvedPath, { recursive: true });
72
+ }
73
+
74
+ if (fs.existsSync(configPath)) {
75
+ console.error('cistack.config.js already exists. Delete it first or edit it manually.');
76
+ process.exit(1);
77
+ }
78
+
79
+ const template = `// cistack.config.js
80
+ // Override auto-detected settings for this project.
81
+ // All fields are optional — omit what you don't need.
82
+
83
+ /** @type {import('cistack').Config} */
84
+ module.exports = {
85
+ // nodeVersion: '20', // Override detected Node.js version
86
+ // packageManager: 'pnpm', // 'npm' | 'yarn' | 'pnpm' | 'bun'
87
+ // hosting: ['Firebase'], // Force a specific hosting provider
88
+ // branches: ['main', 'staging'], // CI branches (default: main, master, develop)
89
+ // outputDir: '.github/workflows', // Where to write workflow files
90
+
91
+ // cache: {
92
+ // npm: true, // enabled by default
93
+ // pip: true,
94
+ // cargo: true,
95
+ // maven: true,
96
+ // gradle: true,
97
+ // go: true,
98
+ // composer: true,
99
+ // },
100
+
101
+ // monorepo: {
102
+ // perPackage: true, // Generate one ci-<name>.yml per workspace
103
+ // },
104
+
105
+ // release: {
106
+ // tool: 'semantic-release', // override release tool detection
107
+ // },
108
+
109
+ // secrets: ['MY_EXTRA_SECRET'], // Document additional secrets in workflow comments
110
+ };
111
+ `;
112
+
113
+ fs.writeFileSync(configPath, template, 'utf8');
114
+ const chalk = require('chalk');
115
+ console.log(chalk.green(`✔ Created cistack.config.js at ${configPath}`));
116
+ console.log(chalk.dim(' Edit the file to override detected settings.'));
117
+ });
118
+
35
119
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cistack",
3
- "version": "1.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Automatically generate GitHub Actions CI/CD pipelines by analysing your codebase",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/ciflow.js",
11
- "test": "node bin/ciflow.js --dry-run"
11
+ "test": "node bin/ciflow.js --dry-run --no-prompt"
12
12
  },
13
13
  "keywords": [
14
14
  "github-actions",
@@ -18,7 +18,14 @@
18
18
  "devops",
19
19
  "firebase",
20
20
  "vercel",
21
- "netlify"
21
+ "netlify",
22
+ "monorepo",
23
+ "dependabot",
24
+ "semantic-release",
25
+ "changesets",
26
+ "caching",
27
+ "turborepo",
28
+ "nx"
22
29
  ],
23
30
  "author": "",
24
31
  "license": "MIT",
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { globSync } = require('glob');
6
+
7
+ /**
8
+ * Resolves workspace packages from monorepo config files.
9
+ *
10
+ * Supported:
11
+ * - package.json#workspaces (npm/yarn workspaces array or { packages: [] })
12
+ * - pnpm-workspace.yaml
13
+ * - turbo.json (uses package.json workspaces in conjunction)
14
+ * - nx.json (discovers apps/* and libs/*)
15
+ * - lerna.json (uses lerna packages array)
16
+ *
17
+ * Returns: Array<{ name: string, relativePath: string, absolutePath: string, packageJson: object|null }>
18
+ */
19
+ class MonorepoAnalyzer {
20
+ constructor(projectPath, codebaseInfo) {
21
+ this.root = projectPath;
22
+ this.info = codebaseInfo;
23
+ this.pkg = codebaseInfo.packageJson || {};
24
+ }
25
+
26
+ async analyze() {
27
+ if (!this.info.hasMonorepo) return [];
28
+
29
+ const workspaceGlobs = this._collectWorkspaceGlobs();
30
+ if (workspaceGlobs.length === 0) return [];
31
+
32
+ const packages = [];
33
+ const seen = new Set();
34
+
35
+ for (const pattern of workspaceGlobs) {
36
+ // Each glob glob like 'packages/*' → expand to actual dirs
37
+ const matches = globSync(pattern, {
38
+ cwd: this.root,
39
+ onlyDirectories: true,
40
+ absolute: false,
41
+ ignore: ['node_modules/**', '**/node_modules/**'],
42
+ });
43
+
44
+ for (const relDir of matches) {
45
+ if (seen.has(relDir)) continue;
46
+ seen.add(relDir);
47
+
48
+ const absPath = path.join(this.root, relDir);
49
+ const pkgJsonPath = path.join(absPath, 'package.json');
50
+ let pkgJson = null;
51
+
52
+ if (fs.existsSync(pkgJsonPath)) {
53
+ try {
54
+ pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
55
+ } catch (_) {}
56
+ }
57
+
58
+ const name = (pkgJson && pkgJson.name) || path.basename(relDir);
59
+
60
+ packages.push({
61
+ name,
62
+ relativePath: relDir,
63
+ absolutePath: absPath,
64
+ packageJson: pkgJson,
65
+ });
66
+ }
67
+ }
68
+
69
+ return packages;
70
+ }
71
+
72
+ _collectWorkspaceGlobs() {
73
+ const globs = [];
74
+
75
+ // 1. package.json workspaces
76
+ if (this.pkg.workspaces) {
77
+ const ws = this.pkg.workspaces;
78
+ if (Array.isArray(ws)) {
79
+ globs.push(...ws);
80
+ } else if (ws.packages) {
81
+ globs.push(...ws.packages);
82
+ }
83
+ }
84
+
85
+ // 2. pnpm-workspace.yaml
86
+ const pnpmWsPath = path.join(this.root, 'pnpm-workspace.yaml');
87
+ if (fs.existsSync(pnpmWsPath)) {
88
+ try {
89
+ const yaml = require('js-yaml');
90
+ const parsed = yaml.load(fs.readFileSync(pnpmWsPath, 'utf8'));
91
+ if (parsed && parsed.packages) {
92
+ globs.push(...parsed.packages);
93
+ }
94
+ } catch (_) {}
95
+ }
96
+
97
+ // 3. lerna.json
98
+ const lernaPath = path.join(this.root, 'lerna.json');
99
+ if (fs.existsSync(lernaPath)) {
100
+ try {
101
+ const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
102
+ if (lerna.packages) globs.push(...lerna.packages);
103
+ } catch (_) {}
104
+ }
105
+
106
+ // 4. nx.json — default convention: apps/* + libs/*
107
+ const nxPath = path.join(this.root, 'nx.json');
108
+ if (fs.existsSync(nxPath) && globs.length === 0) {
109
+ globs.push('apps/*', 'libs/*', 'packages/*');
110
+ }
111
+
112
+ // 5. turbo.json — uses root package.json workspaces (already handled above)
113
+ // If none found yet, use convention
114
+ const turboPath = path.join(this.root, 'turbo.json');
115
+ if (fs.existsSync(turboPath) && globs.length === 0) {
116
+ globs.push('apps/*', 'packages/*');
117
+ }
118
+
119
+ // Deduplicate
120
+ return [...new Set(globs)];
121
+ }
122
+ }
123
+
124
+ module.exports = MonorepoAnalyzer;
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+ const chalk = require('chalk');
7
+
8
+ class WorkflowAnalyzer {
9
+ constructor(projectPath) {
10
+ this.projectPath = projectPath;
11
+ this.workflowsDir = path.join(projectPath, '.github/workflows');
12
+
13
+ // Latest stable versions for common actions
14
+ this.latestVersions = {
15
+ 'actions/checkout': 'v4',
16
+ 'actions/setup-node': 'v4',
17
+ 'actions/setup-python': 'v5',
18
+ 'actions/setup-java': 'v4',
19
+ 'actions/setup-go': 'v5',
20
+ 'actions/upload-artifact': 'v4',
21
+ 'actions/download-artifact': 'v4',
22
+ 'actions/cache': 'v4',
23
+ 'docker/setup-buildx-action': 'v3',
24
+ 'docker/login-action': 'v3',
25
+ 'docker/build-push-action': 'v5',
26
+ 'docker/metadata-action': 'v5',
27
+ 'pnpm/action-setup': 'v3',
28
+ 'codecov/codecov-action': 'v4',
29
+ 'github/codeql-action/init': 'v3',
30
+ 'github/codeql-action/analyze': 'v3',
31
+ 'github/codeql-action/autobuild': 'v3',
32
+ };
33
+ }
34
+
35
+ async audit() {
36
+ const results = {
37
+ files: [],
38
+ totalIssues: 0,
39
+ suggestions: [],
40
+ };
41
+
42
+ if (!fs.existsSync(this.workflowsDir)) {
43
+ return results;
44
+ }
45
+
46
+ const files = fs.readdirSync(this.workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
47
+
48
+ for (const filename of files) {
49
+ const filePath = path.join(this.workflowsDir, filename);
50
+ const content = fs.readFileSync(filePath, 'utf8');
51
+
52
+ try {
53
+ const parsed = yaml.load(content);
54
+ const fileIssues = this._auditFile(filename, parsed, content);
55
+ results.files.push({
56
+ filename,
57
+ issues: fileIssues,
58
+ });
59
+ results.totalIssues += fileIssues.length;
60
+ } catch (err) {
61
+ results.files.push({
62
+ filename,
63
+ error: `Failed to parse YAML: ${err.message}`,
64
+ });
65
+ }
66
+ }
67
+
68
+ return results;
69
+ }
70
+
71
+ _auditFile(filename, parsed, rawContent) {
72
+ const issues = [];
73
+
74
+ // 1. Check for concurrency
75
+ if (!parsed.concurrency) {
76
+ issues.push({
77
+ type: 'missing_concurrency',
78
+ severity: 'medium',
79
+ message: 'Missing concurrency group (highly recommended to prevent redundant runs)',
80
+ fix: 'Add concurrency block with cancel-in-progress: true',
81
+ });
82
+ }
83
+
84
+ // 2. Check for outdated actions
85
+ const actionRegex = /uses:\s*([\w\-\/]+)@([\w\.]+)/g;
86
+ let match;
87
+ while ((match = actionRegex.exec(rawContent)) !== null) {
88
+ const fullAction = match[0];
89
+ const actionName = match[1];
90
+ const currentVersion = match[2];
91
+
92
+ const latest = this.latestVersions[actionName];
93
+ if (latest && this._isOutdated(currentVersion, latest)) {
94
+ issues.push({
95
+ type: 'outdated_action',
96
+ severity: 'low',
97
+ message: `Outdated action: ${actionName}@${currentVersion} (latest is ${latest})`,
98
+ action: actionName,
99
+ current: currentVersion,
100
+ latest: latest,
101
+ fix: `Update to @${latest}`,
102
+ });
103
+ }
104
+ }
105
+
106
+ // 3. Check for node-version (hardcoded vs matrix)
107
+ const rawLines = rawContent.split('\n');
108
+ for (let i = 0; i < rawLines.length; i++) {
109
+ if (rawLines[i].includes('node-version:')) {
110
+ const versionMatch = rawLines[i].match(/node-version:\s*['"]?(\d+)['"]?/);
111
+ if (versionMatch && parseInt(versionMatch[1]) < 18) {
112
+ issues.push({
113
+ type: 'old_node_version',
114
+ severity: 'medium',
115
+ message: `Using end-of-life Node.js version: ${versionMatch[1]}`,
116
+ line: i + 1,
117
+ fix: 'Upgrade to Node.js 18 or 20',
118
+ });
119
+ }
120
+ }
121
+ }
122
+
123
+ // 4. Check for caching
124
+ if (rawContent.includes('actions/setup-node') && !rawContent.includes('cache:')) {
125
+ issues.push({
126
+ type: 'missing_cache',
127
+ severity: 'high',
128
+ message: 'Dependency caching is not enabled in setup-node',
129
+ fix: 'Add cache: "npm" (or yarn/pnpm) to actions/setup-node',
130
+ });
131
+ }
132
+
133
+ return issues;
134
+ }
135
+
136
+ async upgrade(dryRun = false) {
137
+ const results = {
138
+ upgradedFiles: [],
139
+ changes: 0,
140
+ };
141
+
142
+ if (!fs.existsSync(this.workflowsDir)) {
143
+ return results;
144
+ }
145
+
146
+ const files = fs.readdirSync(this.workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
147
+
148
+ for (const filename of files) {
149
+ const filePath = path.join(this.workflowsDir, filename);
150
+ let content = fs.readFileSync(filePath, 'utf8');
151
+ let originalContent = content;
152
+ let fileChanges = 0;
153
+
154
+ for (const [action, latest] of Object.entries(this.latestVersions)) {
155
+ const regex = new RegExp(`uses:\\s*${action}@([\\w\\.]+)`, 'g');
156
+ content = content.replace(regex, (match, version) => {
157
+ if (this._isOutdated(version, latest)) {
158
+ fileChanges++;
159
+ return `uses: ${action}@${latest}`;
160
+ }
161
+ return match;
162
+ });
163
+ }
164
+
165
+ if (fileChanges > 0) {
166
+ if (!dryRun) {
167
+ fs.writeFileSync(filePath, content, 'utf8');
168
+ }
169
+ results.upgradedFiles.push({
170
+ filename,
171
+ changes: fileChanges,
172
+ });
173
+ results.changes += fileChanges;
174
+ }
175
+ }
176
+
177
+ return results;
178
+ }
179
+
180
+ _isOutdated(current, latest) {
181
+ // Simple version comparison for vX formats
182
+ if (current === latest) return false;
183
+
184
+ const currNum = parseInt(current.replace('v', ''));
185
+ const lateNum = parseInt(latest.replace('v', ''));
186
+
187
+ if (!isNaN(currNum) && !isNaN(lateNum)) {
188
+ return currNum < lateNum;
189
+ }
190
+
191
+ return current !== latest; // Fallback for complex tags
192
+ }
193
+ }
194
+
195
+ module.exports = WorkflowAnalyzer;