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 +48 -34
- package/bin/ciflow.js +91 -7
- package/package.json +10 -3
- package/src/analyzers/monorepo.js +124 -0
- package/src/analyzers/workflow.js +195 -0
- package/src/config/loader.js +163 -0
- package/src/detectors/env.js +69 -0
- package/src/detectors/framework.js +37 -12
- package/src/detectors/hosting.js +54 -46
- package/src/detectors/release.js +124 -0
- package/src/generators/dependabot.js +155 -0
- package/src/generators/release.js +195 -0
- package/src/generators/workflow.js +402 -125
- package/src/index.js +247 -54
- package/src/utils/helpers.js +146 -9
|
@@ -4,6 +4,10 @@ const yaml = require('js-yaml');
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Takes all detected signals and produces one or more complete GitHub Actions workflow YAML files.
|
|
7
|
+
* v2.0.0 additions:
|
|
8
|
+
* - Per-language caching (cargo, pip, poetry, m2, gradle, bundler, go, composer)
|
|
9
|
+
* - Monorepo-aware: wraps jobs in a matrix over workspaces or generates per-package files
|
|
10
|
+
* - Env var documentation from .env.example detection
|
|
7
11
|
*/
|
|
8
12
|
class WorkflowGenerator {
|
|
9
13
|
constructor(config, projectPath) {
|
|
@@ -12,6 +16,9 @@ class WorkflowGenerator {
|
|
|
12
16
|
this.languages = config.languages || [];
|
|
13
17
|
this.testing = config.testing || [];
|
|
14
18
|
this.projectPath = projectPath;
|
|
19
|
+
this.envVars = config.envVars || { secrets: [], public: [], all: [], sourceFile: null };
|
|
20
|
+
this.monorepoPackages = config.monorepoPackages || [];
|
|
21
|
+
this.extraConfig = config._config || {}; // raw cistack.config.js
|
|
15
22
|
|
|
16
23
|
// Convenient accessors
|
|
17
24
|
this.primaryLang = this.languages[0] || { name: 'JavaScript', packageManager: 'npm', nodeVersion: '20' };
|
|
@@ -19,18 +26,40 @@ class WorkflowGenerator {
|
|
|
19
26
|
this.e2eTests = this.testing.filter((t) => t.type === 'e2e' && t.confidence > 0.3);
|
|
20
27
|
this.hasDocker = this.hosting.some((h) => h.name === 'Docker');
|
|
21
28
|
this.primaryHosting = this.hosting.filter((h) => h.name !== 'Docker')[0] || null;
|
|
29
|
+
|
|
30
|
+
// Monorepo mode: per-package workflows if configured or if > 1 package
|
|
31
|
+
this.isMonorepo = this.monorepoPackages.length > 0;
|
|
32
|
+
this.perPackageWorkflows = this.isMonorepo && (
|
|
33
|
+
(this.extraConfig.monorepo && this.extraConfig.monorepo.perPackage) ||
|
|
34
|
+
this.monorepoPackages.length > 1
|
|
35
|
+
);
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
generate() {
|
|
25
39
|
const workflows = [];
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
if (this.perPackageWorkflows) {
|
|
42
|
+
// ── Monorepo: one CI file per workspace ─────────────────────────────
|
|
43
|
+
for (const pkg of this.monorepoPackages) {
|
|
44
|
+
workflows.push({
|
|
45
|
+
filename: `ci-${this._slugify(pkg.name)}.yml`,
|
|
46
|
+
content: this._buildCIWorkflow(pkg),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Root-level CI file (matrix over all packages)
|
|
50
|
+
workflows.push({
|
|
51
|
+
filename: 'ci.yml',
|
|
52
|
+
content: this._buildMonorepoRootCI(),
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
// ── Standard: single CI workflow ────────────────────────────────────
|
|
56
|
+
workflows.push({
|
|
57
|
+
filename: 'ci.yml',
|
|
58
|
+
content: this._buildCIWorkflow(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
32
61
|
|
|
33
|
-
// ── 2. Deploy / CD workflow
|
|
62
|
+
// ── 2. Deploy / CD workflow ──────────────────────────────────────────
|
|
34
63
|
if (this.primaryHosting) {
|
|
35
64
|
workflows.push({
|
|
36
65
|
filename: 'deploy.yml',
|
|
@@ -38,7 +67,7 @@ class WorkflowGenerator {
|
|
|
38
67
|
});
|
|
39
68
|
}
|
|
40
69
|
|
|
41
|
-
// ── 3. Docker image build+push
|
|
70
|
+
// ── 3. Docker image build+push ───────────────────────────────────────
|
|
42
71
|
if (this.hasDocker) {
|
|
43
72
|
workflows.push({
|
|
44
73
|
filename: 'docker.yml',
|
|
@@ -46,7 +75,7 @@ class WorkflowGenerator {
|
|
|
46
75
|
});
|
|
47
76
|
}
|
|
48
77
|
|
|
49
|
-
// ── 4.
|
|
78
|
+
// ── 4. Security audit ────────────────────────────────────────────────
|
|
50
79
|
workflows.push({
|
|
51
80
|
filename: 'security.yml',
|
|
52
81
|
content: this._buildSecurityWorkflow(),
|
|
@@ -59,40 +88,38 @@ class WorkflowGenerator {
|
|
|
59
88
|
// CI Workflow
|
|
60
89
|
// ══════════════════════════════════════════════════════════════════════════
|
|
61
90
|
|
|
62
|
-
_buildCIWorkflow() {
|
|
63
|
-
const lang = this.
|
|
91
|
+
_buildCIWorkflow(pkg = null) {
|
|
92
|
+
const lang = this._langForPackage(pkg);
|
|
64
93
|
const jobs = {};
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
const lintSteps = [
|
|
68
|
-
this._stepCheckout(),
|
|
69
|
-
...this._setupSteps(lang),
|
|
70
|
-
this._stepInstallDeps(lang),
|
|
71
|
-
this._stepLint(lang),
|
|
72
|
-
].filter(Boolean);
|
|
95
|
+
const branches = this.extraConfig.branches || ['main', 'master', 'develop'];
|
|
73
96
|
|
|
97
|
+
// ── lint job ──────────────────────────────────────────────────────────
|
|
74
98
|
jobs.lint = {
|
|
75
99
|
name: '🔍 Lint & Format',
|
|
76
100
|
'runs-on': 'ubuntu-latest',
|
|
77
|
-
steps:
|
|
101
|
+
steps: [
|
|
102
|
+
this._stepCheckout(),
|
|
103
|
+
...this._setupSteps(lang),
|
|
104
|
+
this._stepInstallDeps(lang),
|
|
105
|
+
this._stepLint(lang),
|
|
106
|
+
].filter(Boolean),
|
|
78
107
|
};
|
|
79
108
|
|
|
80
109
|
// ── test job ──────────────────────────────────────────────────────────
|
|
81
110
|
if (this.unitTests.length > 0) {
|
|
82
111
|
const testMatrix = this._testMatrix(lang);
|
|
83
|
-
const testSteps = [
|
|
84
|
-
this._stepCheckout(),
|
|
85
|
-
...this._setupSteps(lang),
|
|
86
|
-
this._stepInstallDeps(lang),
|
|
87
|
-
...this._unitTestSteps(lang),
|
|
88
|
-
this._stepUploadCoverage(),
|
|
89
|
-
].filter(Boolean);
|
|
90
|
-
|
|
91
112
|
jobs.test = {
|
|
92
113
|
name: '🧪 Unit Tests',
|
|
93
114
|
'runs-on': 'ubuntu-latest',
|
|
94
115
|
...(testMatrix ? { strategy: testMatrix } : {}),
|
|
95
|
-
steps:
|
|
116
|
+
steps: [
|
|
117
|
+
this._stepCheckout(),
|
|
118
|
+
...this._setupSteps(lang),
|
|
119
|
+
this._stepInstallDeps(lang),
|
|
120
|
+
...this._unitTestSteps(lang),
|
|
121
|
+
this._stepUploadCoverage(),
|
|
122
|
+
].filter(Boolean),
|
|
96
123
|
};
|
|
97
124
|
}
|
|
98
125
|
|
|
@@ -102,10 +129,7 @@ class WorkflowGenerator {
|
|
|
102
129
|
jobs.build = {
|
|
103
130
|
name: '🏗️ Build',
|
|
104
131
|
'runs-on': 'ubuntu-latest',
|
|
105
|
-
needs: [
|
|
106
|
-
'lint',
|
|
107
|
-
...(jobs.test ? ['test'] : []),
|
|
108
|
-
],
|
|
132
|
+
needs: ['lint', ...(jobs.test ? ['test'] : [])],
|
|
109
133
|
steps: [
|
|
110
134
|
this._stepCheckout(),
|
|
111
135
|
...this._setupSteps(lang),
|
|
@@ -119,7 +143,7 @@ class WorkflowGenerator {
|
|
|
119
143
|
// ── e2e job ───────────────────────────────────────────────────────────
|
|
120
144
|
if (this.e2eTests.length > 0) {
|
|
121
145
|
const e2eTest = this.e2eTests[0];
|
|
122
|
-
jobs
|
|
146
|
+
jobs.e2e = {
|
|
123
147
|
name: '🎭 E2E Tests',
|
|
124
148
|
'runs-on': 'ubuntu-latest',
|
|
125
149
|
needs: ['build'],
|
|
@@ -127,7 +151,9 @@ class WorkflowGenerator {
|
|
|
127
151
|
this._stepCheckout(),
|
|
128
152
|
...this._setupSteps(lang),
|
|
129
153
|
this._stepInstallDeps(lang),
|
|
130
|
-
...(e2eTest.name === 'Playwright'
|
|
154
|
+
...(e2eTest.name === 'Playwright'
|
|
155
|
+
? [{ name: 'Install Playwright browsers', run: 'npx playwright install --with-deps' }]
|
|
156
|
+
: []),
|
|
131
157
|
{ name: `Run ${e2eTest.name}`, run: e2eTest.command },
|
|
132
158
|
{
|
|
133
159
|
name: 'Upload E2E report',
|
|
@@ -144,19 +170,104 @@ class WorkflowGenerator {
|
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
const workflow = {
|
|
147
|
-
name: 'CI',
|
|
173
|
+
name: pkg ? `CI — ${pkg.name}` : 'CI',
|
|
148
174
|
on: {
|
|
149
|
-
push: {
|
|
150
|
-
|
|
175
|
+
push: {
|
|
176
|
+
branches,
|
|
177
|
+
...(pkg ? { paths: [`${pkg.relativePath}/**`] } : {}),
|
|
178
|
+
},
|
|
179
|
+
pull_request: {
|
|
180
|
+
branches,
|
|
181
|
+
...(pkg ? { paths: [`${pkg.relativePath}/**`] } : {}),
|
|
182
|
+
},
|
|
151
183
|
},
|
|
152
|
-
|
|
184
|
+
concurrency: {
|
|
153
185
|
group: '${{ github.workflow }}-${{ github.ref }}',
|
|
154
186
|
'cancel-in-progress': true,
|
|
155
187
|
},
|
|
156
188
|
jobs,
|
|
157
189
|
};
|
|
158
190
|
|
|
159
|
-
|
|
191
|
+
const envComment = this._envComment();
|
|
192
|
+
const header =
|
|
193
|
+
`# Generated by cistack v2.0.0 — https://github.com/cistack\n` +
|
|
194
|
+
`# CI Pipeline: lint → test → build${this.e2eTests.length > 0 ? ' → e2e' : ''}\n` +
|
|
195
|
+
envComment +
|
|
196
|
+
`\n`;
|
|
197
|
+
|
|
198
|
+
return this._toYaml(workflow, header);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Monorepo root CI (matrix over all workspaces) ────────────────────────
|
|
202
|
+
_buildMonorepoRootCI() {
|
|
203
|
+
const lang = this.primaryLang;
|
|
204
|
+
const branches = this.extraConfig.branches || ['main', 'master', 'develop'];
|
|
205
|
+
const pkgPaths = this.monorepoPackages.map((p) => p.relativePath);
|
|
206
|
+
|
|
207
|
+
const workflow = {
|
|
208
|
+
name: 'CI — Monorepo',
|
|
209
|
+
on: {
|
|
210
|
+
push: { branches },
|
|
211
|
+
pull_request: { branches },
|
|
212
|
+
},
|
|
213
|
+
concurrency: {
|
|
214
|
+
group: '${{ github.workflow }}-${{ github.ref }}',
|
|
215
|
+
'cancel-in-progress': true,
|
|
216
|
+
},
|
|
217
|
+
jobs: {
|
|
218
|
+
ci: {
|
|
219
|
+
name: '🧪 ${{ matrix.package }}',
|
|
220
|
+
'runs-on': 'ubuntu-latest',
|
|
221
|
+
strategy: {
|
|
222
|
+
matrix: {
|
|
223
|
+
package: pkgPaths,
|
|
224
|
+
},
|
|
225
|
+
'fail-fast': false,
|
|
226
|
+
},
|
|
227
|
+
steps: [
|
|
228
|
+
this._stepCheckout(),
|
|
229
|
+
...this._setupSteps(lang),
|
|
230
|
+
{
|
|
231
|
+
name: 'Install dependencies',
|
|
232
|
+
run: lang.packageManager === 'pnpm'
|
|
233
|
+
? 'pnpm --filter ${{ matrix.package }} install --frozen-lockfile'
|
|
234
|
+
: lang.packageManager === 'yarn'
|
|
235
|
+
? 'yarn workspace ${{ matrix.package }} install'
|
|
236
|
+
: 'npm ci --workspace=${{ matrix.package }}',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'Lint',
|
|
240
|
+
run: lang.packageManager === 'pnpm'
|
|
241
|
+
? 'pnpm --filter ${{ matrix.package }} run lint --if-present'
|
|
242
|
+
: lang.packageManager === 'yarn'
|
|
243
|
+
? 'yarn workspace ${{ matrix.package }} run lint || true'
|
|
244
|
+
: 'npm run --workspace=${{ matrix.package }} lint || true',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'Test',
|
|
248
|
+
run: lang.packageManager === 'pnpm'
|
|
249
|
+
? 'pnpm --filter ${{ matrix.package }} run test --if-present'
|
|
250
|
+
: lang.packageManager === 'yarn'
|
|
251
|
+
? 'yarn workspace ${{ matrix.package }} run test || true'
|
|
252
|
+
: 'npm run --workspace=${{ matrix.package }} test || true',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'Build',
|
|
256
|
+
run: lang.packageManager === 'pnpm'
|
|
257
|
+
? 'pnpm --filter ${{ matrix.package }} run build --if-present'
|
|
258
|
+
: lang.packageManager === 'yarn'
|
|
259
|
+
? 'yarn workspace ${{ matrix.package }} run build || true'
|
|
260
|
+
: 'npm run --workspace=${{ matrix.package }} build || true',
|
|
261
|
+
},
|
|
262
|
+
].filter(Boolean),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return this._toYaml(
|
|
268
|
+
workflow,
|
|
269
|
+
'# Generated by cistack v2.0.0 — https://github.com/cistack\n# Monorepo CI — matrix over all workspaces\n\n'
|
|
270
|
+
);
|
|
160
271
|
}
|
|
161
272
|
|
|
162
273
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -166,6 +277,7 @@ class WorkflowGenerator {
|
|
|
166
277
|
_buildDeployWorkflow() {
|
|
167
278
|
const h = this.primaryHosting;
|
|
168
279
|
const lang = this.primaryLang;
|
|
280
|
+
const branches = this.extraConfig.branches || ['main', 'master'];
|
|
169
281
|
|
|
170
282
|
const preDeploySteps = [
|
|
171
283
|
this._stepCheckout(),
|
|
@@ -173,31 +285,56 @@ class WorkflowGenerator {
|
|
|
173
285
|
this._stepInstallDeps(lang),
|
|
174
286
|
].filter(Boolean);
|
|
175
287
|
|
|
176
|
-
const deploySteps = this._hostingDeploySteps(h, lang);
|
|
288
|
+
const deploySteps = this._hostingDeploySteps(h, lang, false); // production
|
|
289
|
+
const previewSteps = this._hostingDeploySteps(h, lang, true); // preview
|
|
177
290
|
|
|
178
291
|
const jobs = {
|
|
179
292
|
deploy: {
|
|
180
|
-
name: `🚀 Deploy → ${h.name}`,
|
|
293
|
+
name: `🚀 Deploy → ${h.name} (Production)`,
|
|
294
|
+
if: "github.event_name == 'push' || github.event_name == 'workflow_dispatch'",
|
|
181
295
|
'runs-on': 'ubuntu-latest',
|
|
182
296
|
environment: 'production',
|
|
183
297
|
steps: [...preDeploySteps, ...deploySteps].filter(Boolean),
|
|
184
298
|
},
|
|
185
299
|
};
|
|
186
300
|
|
|
187
|
-
|
|
188
|
-
|
|
301
|
+
// Add preview job if supported
|
|
302
|
+
if (previewSteps.length > 0) {
|
|
303
|
+
jobs.preview = {
|
|
304
|
+
name: `✨ Deploy → ${h.name} (Preview)`,
|
|
305
|
+
if: "github.event_name == 'pull_request'",
|
|
306
|
+
'runs-on': 'ubuntu-latest',
|
|
307
|
+
environment: 'preview',
|
|
308
|
+
steps: [...preDeploySteps, ...previewSteps].filter(Boolean),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const allSecrets = [
|
|
313
|
+
...(h.secrets || []),
|
|
314
|
+
...this.envVars.secrets,
|
|
315
|
+
];
|
|
316
|
+
const uniqueSecrets = [...new Set(allSecrets)];
|
|
317
|
+
|
|
318
|
+
const secretsDoc = uniqueSecrets.length > 0
|
|
319
|
+
? `# Required secrets: ${uniqueSecrets.join(', ')}\n# Add these at: Settings → Secrets and Variables → Actions\n\n`
|
|
189
320
|
: '';
|
|
190
321
|
|
|
322
|
+
const envComment = this._envComment();
|
|
323
|
+
|
|
191
324
|
const workflow = {
|
|
192
325
|
name: `Deploy to ${h.name}`,
|
|
193
326
|
on: {
|
|
194
|
-
push: { branches:
|
|
327
|
+
push: { branches: branches.filter((b) => b !== 'develop') },
|
|
328
|
+
pull_request: { branches },
|
|
195
329
|
workflow_dispatch: {},
|
|
196
330
|
},
|
|
197
331
|
jobs,
|
|
198
332
|
};
|
|
199
333
|
|
|
200
|
-
return this._toYaml(
|
|
334
|
+
return this._toYaml(
|
|
335
|
+
workflow,
|
|
336
|
+
`# Generated by cistack v2.0.0\n# Deploy Pipeline → ${h.name}\n${secretsDoc}${envComment}\n`
|
|
337
|
+
);
|
|
201
338
|
}
|
|
202
339
|
|
|
203
340
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -274,7 +411,7 @@ class WorkflowGenerator {
|
|
|
274
411
|
},
|
|
275
412
|
};
|
|
276
413
|
|
|
277
|
-
return this._toYaml(workflow, '# Generated by cistack\n# Docker image build and push to GHCR\n\n');
|
|
414
|
+
return this._toYaml(workflow, '# Generated by cistack v2.0.0\n# Docker image build and push to GHCR\n\n');
|
|
278
415
|
}
|
|
279
416
|
|
|
280
417
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -289,7 +426,14 @@ class WorkflowGenerator {
|
|
|
289
426
|
steps.push(
|
|
290
427
|
...this._setupSteps(lang),
|
|
291
428
|
this._stepInstallDeps(lang),
|
|
292
|
-
{
|
|
429
|
+
{
|
|
430
|
+
name: 'Audit dependencies',
|
|
431
|
+
run:
|
|
432
|
+
lang.packageManager === 'npm' ? 'npm audit --audit-level=high' :
|
|
433
|
+
lang.packageManager === 'yarn' ? 'yarn audit --level high' :
|
|
434
|
+
lang.packageManager === 'pnpm' ? 'pnpm audit --audit-level high' :
|
|
435
|
+
'npm audit --audit-level=high',
|
|
436
|
+
},
|
|
293
437
|
);
|
|
294
438
|
}
|
|
295
439
|
|
|
@@ -301,19 +445,21 @@ class WorkflowGenerator {
|
|
|
301
445
|
);
|
|
302
446
|
}
|
|
303
447
|
|
|
448
|
+
if (lang.name === 'Rust') {
|
|
449
|
+
steps.push(
|
|
450
|
+
{ name: 'Set up Rust', uses: 'dtolnay/rust-toolchain@stable' },
|
|
451
|
+
{ name: 'Run cargo audit', run: 'cargo install cargo-audit && cargo audit' },
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
304
455
|
// CodeQL analysis
|
|
305
456
|
steps.push(
|
|
306
457
|
{
|
|
307
458
|
name: 'Initialize CodeQL',
|
|
308
459
|
uses: 'github/codeql-action/init@v3',
|
|
309
|
-
with: {
|
|
310
|
-
languages: this._codeQLLanguage(lang.name),
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
name: 'Perform CodeQL Analysis',
|
|
315
|
-
uses: 'github/codeql-action/analyze@v3',
|
|
460
|
+
with: { languages: this._codeQLLanguage(lang.name) },
|
|
316
461
|
},
|
|
462
|
+
{ name: 'Perform CodeQL Analysis', uses: 'github/codeql-action/analyze@v3' },
|
|
317
463
|
);
|
|
318
464
|
|
|
319
465
|
const workflow = {
|
|
@@ -321,7 +467,7 @@ class WorkflowGenerator {
|
|
|
321
467
|
on: {
|
|
322
468
|
push: { branches: ['main', 'master'] },
|
|
323
469
|
pull_request: { branches: ['main', 'master'] },
|
|
324
|
-
schedule: [{ cron: '0 6 * * 1' }],
|
|
470
|
+
schedule: [{ cron: '0 6 * * 1' }],
|
|
325
471
|
},
|
|
326
472
|
jobs: {
|
|
327
473
|
security: {
|
|
@@ -337,7 +483,7 @@ class WorkflowGenerator {
|
|
|
337
483
|
},
|
|
338
484
|
};
|
|
339
485
|
|
|
340
|
-
return this._toYaml(workflow, '# Generated by cistack\n# Security: dependency audit + CodeQL analysis (runs weekly)\n\n');
|
|
486
|
+
return this._toYaml(workflow, '# Generated by cistack v2.0.0\n# Security: dependency audit + CodeQL analysis (runs weekly)\n\n');
|
|
341
487
|
}
|
|
342
488
|
|
|
343
489
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -348,110 +494,185 @@ class WorkflowGenerator {
|
|
|
348
494
|
return { name: 'Checkout code', uses: 'actions/checkout@v4', with: { 'fetch-depth': 0 } };
|
|
349
495
|
}
|
|
350
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Returns setup + cache steps for the given language.
|
|
499
|
+
* v2.0.0: added explicit caching for pip, poetry, cargo, maven, gradle, bundler, go, composer.
|
|
500
|
+
*/
|
|
351
501
|
_setupSteps(lang) {
|
|
352
502
|
const steps = [];
|
|
503
|
+
const cacheOverride = this.extraConfig.cache || {};
|
|
504
|
+
|
|
505
|
+
// ── JavaScript / TypeScript ──────────────────────────────────────────
|
|
353
506
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
507
|
+
if (lang.packageManager === 'pnpm') {
|
|
508
|
+
steps.push({ name: 'Install pnpm', uses: 'pnpm/action-setup@v3', with: { version: 'latest' } });
|
|
509
|
+
}
|
|
354
510
|
steps.push({
|
|
355
511
|
name: 'Set up Node.js',
|
|
356
512
|
uses: 'actions/setup-node@v4',
|
|
357
513
|
with: {
|
|
358
514
|
'node-version': lang.nodeVersion || '20',
|
|
359
|
-
|
|
515
|
+
// Use native caching in setup-node
|
|
516
|
+
cache: cacheOverride.npm !== false ? (lang.packageManager === 'yarn' ? 'yarn' : lang.packageManager === 'pnpm' ? 'pnpm' : 'npm') : undefined,
|
|
360
517
|
},
|
|
361
518
|
});
|
|
362
|
-
if (lang.packageManager === 'pnpm') {
|
|
363
|
-
steps.unshift({ name: 'Install pnpm', uses: 'pnpm/action-setup@v3', with: { version: 'latest' } });
|
|
364
|
-
}
|
|
365
519
|
}
|
|
520
|
+
|
|
521
|
+
// ── Python ───────────────────────────────────────────────────────────
|
|
366
522
|
if (lang.name === 'Python') {
|
|
367
|
-
steps.push({
|
|
523
|
+
steps.push({
|
|
524
|
+
name: 'Set up Python',
|
|
525
|
+
uses: 'actions/setup-python@v5',
|
|
526
|
+
with: {
|
|
527
|
+
'python-version': '3.x',
|
|
528
|
+
// Native caching for pip/poetry
|
|
529
|
+
cache: cacheOverride.pip !== false ? (lang.packageManager === 'poetry' ? 'poetry' : 'pip') : undefined
|
|
530
|
+
},
|
|
531
|
+
});
|
|
368
532
|
}
|
|
533
|
+
|
|
534
|
+
// ── Go ───────────────────────────────────────────────────────────────
|
|
369
535
|
if (lang.name === 'Go') {
|
|
370
|
-
steps.push({
|
|
536
|
+
steps.push({
|
|
537
|
+
name: 'Set up Go',
|
|
538
|
+
uses: 'actions/setup-go@v5',
|
|
539
|
+
with: {
|
|
540
|
+
'go-version': 'stable',
|
|
541
|
+
cache: cacheOverride.go !== false
|
|
542
|
+
},
|
|
543
|
+
});
|
|
371
544
|
}
|
|
545
|
+
|
|
546
|
+
// ── Java / Kotlin ─────────────────────────────────────────────────────
|
|
372
547
|
if (lang.name === 'Java' || lang.name === 'Kotlin') {
|
|
373
|
-
steps.push({
|
|
548
|
+
steps.push({
|
|
549
|
+
name: 'Set up JDK',
|
|
550
|
+
uses: 'actions/setup-java@v4',
|
|
551
|
+
with: {
|
|
552
|
+
'java-version': '21',
|
|
553
|
+
distribution: 'temurin',
|
|
554
|
+
// Native caching for maven/gradle
|
|
555
|
+
cache: cacheOverride.maven !== false ? (lang.packageManager === 'gradle' ? 'gradle' : 'maven') : undefined
|
|
556
|
+
},
|
|
557
|
+
});
|
|
374
558
|
}
|
|
559
|
+
|
|
560
|
+
// ── Ruby ─────────────────────────────────────────────────────────────
|
|
375
561
|
if (lang.name === 'Ruby') {
|
|
376
|
-
steps.push({
|
|
562
|
+
steps.push({
|
|
563
|
+
name: 'Set up Ruby',
|
|
564
|
+
uses: 'ruby/setup-ruby@v1',
|
|
565
|
+
with: { 'bundler-cache': cacheOverride.bundler !== false },
|
|
566
|
+
});
|
|
377
567
|
}
|
|
568
|
+
|
|
569
|
+
// ── Rust ─────────────────────────────────────────────────────────────
|
|
378
570
|
if (lang.name === 'Rust') {
|
|
379
571
|
steps.push({ name: 'Set up Rust', uses: 'dtolnay/rust-toolchain@stable' });
|
|
572
|
+
|
|
573
|
+
if (cacheOverride.cargo !== false) {
|
|
574
|
+
steps.push({
|
|
575
|
+
name: 'Cache Cargo registry',
|
|
576
|
+
uses: 'actions/cache@v4',
|
|
577
|
+
with: {
|
|
578
|
+
path: [
|
|
579
|
+
'~/.cargo/registry',
|
|
580
|
+
'~/.cargo/git',
|
|
581
|
+
'target',
|
|
582
|
+
].join('\n'),
|
|
583
|
+
key: "${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}",
|
|
584
|
+
'restore-keys': '${{ runner.os }}-cargo-',
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── PHP ───────────────────────────────────────────────────────────────
|
|
591
|
+
if (lang.name === 'PHP') {
|
|
592
|
+
if (cacheOverride.composer !== false) {
|
|
593
|
+
steps.push({
|
|
594
|
+
name: 'Get Composer cache directory',
|
|
595
|
+
id: 'composer-cache',
|
|
596
|
+
run: 'echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT',
|
|
597
|
+
});
|
|
598
|
+
steps.push({
|
|
599
|
+
name: 'Cache Composer packages',
|
|
600
|
+
uses: 'actions/cache@v4',
|
|
601
|
+
with: {
|
|
602
|
+
path: '${{ steps.composer-cache.outputs.dir }}',
|
|
603
|
+
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}",
|
|
604
|
+
'restore-keys': '${{ runner.os }}-composer-',
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
}
|
|
380
608
|
}
|
|
609
|
+
|
|
381
610
|
return steps;
|
|
382
611
|
}
|
|
383
612
|
|
|
384
613
|
_stepInstallDeps(lang) {
|
|
385
614
|
const pm = lang.packageManager;
|
|
386
|
-
if (pm === 'npm')
|
|
387
|
-
if (pm === 'yarn')
|
|
388
|
-
if (pm === 'pnpm')
|
|
389
|
-
if (pm === 'bun')
|
|
390
|
-
if (pm === 'pip')
|
|
391
|
-
if (pm === 'poetry')
|
|
392
|
-
if (pm === 'pipenv')
|
|
615
|
+
if (pm === 'npm') return { name: 'Install dependencies', run: 'npm ci' };
|
|
616
|
+
if (pm === 'yarn') return { name: 'Install dependencies', run: 'yarn install --frozen-lockfile' };
|
|
617
|
+
if (pm === 'pnpm') return { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' };
|
|
618
|
+
if (pm === 'bun') return { name: 'Install dependencies', run: 'bun install' };
|
|
619
|
+
if (pm === 'pip') return { name: 'Install dependencies', run: 'pip install -r requirements.txt' };
|
|
620
|
+
if (pm === 'poetry') return { name: 'Install dependencies', run: 'pip install poetry && poetry install' };
|
|
621
|
+
if (pm === 'pipenv') return { name: 'Install dependencies', run: 'pip install pipenv && pipenv install --dev' };
|
|
393
622
|
if (pm === 'bundler') return { name: 'Install dependencies', run: 'bundle install' };
|
|
394
|
-
if (pm === 'go mod')
|
|
395
|
-
if (pm === 'cargo')
|
|
396
|
-
if (pm === 'maven')
|
|
623
|
+
if (pm === 'go mod') return { name: 'Download modules', run: 'go mod download' };
|
|
624
|
+
if (pm === 'cargo') return null; // Cargo handles deps on build/test
|
|
625
|
+
if (pm === 'maven') return { name: 'Install dependencies', run: 'mvn -B dependency:resolve --no-transfer-progress' };
|
|
626
|
+
if (pm === 'gradle') return { name: 'Install dependencies', run: './gradlew dependencies' };
|
|
397
627
|
if (pm === 'composer') return { name: 'Install dependencies', run: 'composer install --no-interaction --prefer-dist --optimize-autoloader' };
|
|
398
628
|
return { name: 'Install dependencies', run: 'npm ci' };
|
|
399
629
|
}
|
|
400
630
|
|
|
401
631
|
_stepLint(lang) {
|
|
402
632
|
const pm = lang.packageManager || 'npm';
|
|
403
|
-
const scripts = (this.languages[0] || {}).scripts || {};
|
|
404
633
|
|
|
405
634
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
406
|
-
// Pick the right lint command
|
|
407
635
|
const lintScript = this._findScript(['lint', 'lint:ci', 'eslint']);
|
|
408
|
-
const typeCheck
|
|
409
|
-
const format
|
|
636
|
+
const typeCheck = this._findScript(['type-check', 'typecheck', 'tsc']);
|
|
637
|
+
const format = this._findScript(['format:check', 'prettier:check', 'fmt:check']);
|
|
638
|
+
const runPfx = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run';
|
|
410
639
|
|
|
411
640
|
const cmds = [];
|
|
412
|
-
if (lintScript) cmds.push(`${
|
|
413
|
-
if (typeCheck)
|
|
414
|
-
if (format)
|
|
641
|
+
if (lintScript) cmds.push(`${runPfx} ${lintScript}`);
|
|
642
|
+
if (typeCheck) cmds.push(`${runPfx} ${typeCheck}`);
|
|
643
|
+
if (format) cmds.push(`${runPfx} ${format}`);
|
|
415
644
|
if (cmds.length === 0) cmds.push('npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0');
|
|
416
645
|
|
|
417
646
|
return { name: 'Lint', run: cmds.join('\n') };
|
|
418
647
|
}
|
|
419
648
|
|
|
420
649
|
if (lang.name === 'Python') return { name: 'Lint', run: 'pip install flake8 && flake8 .' };
|
|
421
|
-
if (lang.name === 'Go')
|
|
422
|
-
if (lang.name === 'Rust')
|
|
423
|
-
if (lang.name === 'Ruby')
|
|
424
|
-
if (lang.name === 'PHP')
|
|
650
|
+
if (lang.name === 'Go') return { name: 'Lint', run: 'gofmt -d . && go vet ./...' };
|
|
651
|
+
if (lang.name === 'Rust') return { name: 'Lint', run: 'cargo clippy -- -D warnings && cargo fmt --check' };
|
|
652
|
+
if (lang.name === 'Ruby') return { name: 'Lint', run: 'gem install rubocop && rubocop' };
|
|
653
|
+
if (lang.name === 'PHP') return { name: 'Lint', run: 'vendor/bin/phpcs' };
|
|
425
654
|
|
|
426
655
|
return null;
|
|
427
656
|
}
|
|
428
657
|
|
|
429
658
|
_unitTestSteps(lang) {
|
|
430
|
-
return this.unitTests.map((t) => ({
|
|
431
|
-
name: `Run ${t.name}`,
|
|
432
|
-
run: t.command,
|
|
433
|
-
}));
|
|
659
|
+
return this.unitTests.map((t) => ({ name: `Run ${t.name}`, run: t.command }));
|
|
434
660
|
}
|
|
435
661
|
|
|
436
662
|
_buildSteps(lang) {
|
|
437
663
|
const pm = lang.packageManager || 'npm';
|
|
438
664
|
const buildScript = this._findScript(['build', 'build:prod']);
|
|
439
|
-
|
|
440
665
|
if (!buildScript && !['Go', 'Rust', 'Java', 'Kotlin'].includes(lang.name)) return [];
|
|
441
666
|
|
|
442
667
|
const steps = [];
|
|
443
|
-
|
|
444
668
|
if (['JavaScript', 'TypeScript'].includes(lang.name) && buildScript) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
run: `${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm'} run ${buildScript}`,
|
|
448
|
-
env: { NODE_ENV: 'production' },
|
|
449
|
-
});
|
|
669
|
+
const runPfx = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm';
|
|
670
|
+
steps.push({ name: 'Build', run: `${runPfx} run ${buildScript}`, env: { NODE_ENV: 'production' } });
|
|
450
671
|
}
|
|
451
|
-
if (lang.name === 'Go')
|
|
452
|
-
if (lang.name === 'Rust')
|
|
453
|
-
if (lang.name === 'Java')
|
|
454
|
-
if (lang.name === '
|
|
672
|
+
if (lang.name === 'Go') steps.push({ name: 'Build', run: 'go build -v ./...' });
|
|
673
|
+
if (lang.name === 'Rust') steps.push({ name: 'Build', run: 'cargo build --release' });
|
|
674
|
+
if (lang.name === 'Java') steps.push({ name: 'Build', run: 'mvn -B package --no-transfer-progress -DskipTests' });
|
|
675
|
+
if (lang.name === 'Kotlin') steps.push({ name: 'Build', run: './gradlew build -x test' });
|
|
455
676
|
|
|
456
677
|
return steps;
|
|
457
678
|
}
|
|
@@ -461,11 +682,7 @@ class WorkflowGenerator {
|
|
|
461
682
|
return {
|
|
462
683
|
name: 'Upload build artifact',
|
|
463
684
|
uses: 'actions/upload-artifact@v4',
|
|
464
|
-
with: {
|
|
465
|
-
name: 'build-output',
|
|
466
|
-
path: buildDir,
|
|
467
|
-
'retention-days': 1,
|
|
468
|
-
},
|
|
685
|
+
with: { name: 'build-output', path: buildDir, 'retention-days': 1 },
|
|
469
686
|
};
|
|
470
687
|
}
|
|
471
688
|
|
|
@@ -483,12 +700,7 @@ class WorkflowGenerator {
|
|
|
483
700
|
|
|
484
701
|
_testMatrix(lang) {
|
|
485
702
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
486
|
-
return {
|
|
487
|
-
matrix: {
|
|
488
|
-
'node-version': ['18.x', '20.x', '22.x'],
|
|
489
|
-
},
|
|
490
|
-
'fail-fast': false,
|
|
491
|
-
};
|
|
703
|
+
return { matrix: { 'node-version': ['18.x', '20.x', '22.x'] }, 'fail-fast': false };
|
|
492
704
|
}
|
|
493
705
|
if (lang.name === 'Python') {
|
|
494
706
|
return { matrix: { 'python-version': ['3.10', '3.11', '3.12'] }, 'fail-fast': false };
|
|
@@ -500,7 +712,7 @@ class WorkflowGenerator {
|
|
|
500
712
|
// Hosting-specific deploy steps
|
|
501
713
|
// ══════════════════════════════════════════════════════════════════════════
|
|
502
714
|
|
|
503
|
-
_hostingDeploySteps(h, lang) {
|
|
715
|
+
_hostingDeploySteps(h, lang, isPreview = false) {
|
|
504
716
|
const steps = [];
|
|
505
717
|
const buildScript = this._findScript(['build', 'build:prod']);
|
|
506
718
|
const pm = lang.packageManager || 'npm';
|
|
@@ -512,23 +724,24 @@ class WorkflowGenerator {
|
|
|
512
724
|
steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
513
725
|
}
|
|
514
726
|
steps.push({
|
|
515
|
-
name: 'Deploy to Firebase',
|
|
727
|
+
name: isPreview ? 'Deploy Preview' : 'Deploy to Firebase',
|
|
516
728
|
uses: 'FirebaseExtended/action-hosting-deploy@v0',
|
|
517
729
|
with: {
|
|
518
730
|
repoToken: '${{ secrets.GITHUB_TOKEN }}',
|
|
519
731
|
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}',
|
|
520
|
-
channelId: 'live',
|
|
732
|
+
channelId: isPreview ? 'preview-${{ github.event.number }}' : 'live',
|
|
521
733
|
},
|
|
522
734
|
});
|
|
523
735
|
break;
|
|
524
736
|
}
|
|
525
737
|
|
|
526
738
|
case 'Vercel': {
|
|
739
|
+
const prodFlag = isPreview ? '' : '--prod';
|
|
527
740
|
steps.push(
|
|
528
741
|
{ name: 'Install Vercel CLI', run: 'npm install -g vercel' },
|
|
529
|
-
{ name: 'Pull Vercel environment', run:
|
|
530
|
-
{ name: 'Build project', run:
|
|
531
|
-
{ name: 'Deploy to Vercel', run:
|
|
742
|
+
{ name: 'Pull Vercel environment', run: `vercel pull --yes --environment=${isPreview ? 'preview' : 'production'} --token=\${{ secrets.VERCEL_TOKEN }}` },
|
|
743
|
+
{ name: 'Build project', run: `vercel build ${prodFlag} --token=\${{ secrets.VERCEL_TOKEN }}` },
|
|
744
|
+
{ name: 'Deploy to Vercel', run: `vercel deploy --prebuilt ${prodFlag} --token=\${{ secrets.VERCEL_TOKEN }}` },
|
|
532
745
|
);
|
|
533
746
|
break;
|
|
534
747
|
}
|
|
@@ -538,15 +751,17 @@ class WorkflowGenerator {
|
|
|
538
751
|
steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
539
752
|
}
|
|
540
753
|
steps.push({
|
|
541
|
-
name: 'Deploy to Netlify',
|
|
754
|
+
name: isPreview ? 'Deploy Preview' : 'Deploy to Netlify',
|
|
542
755
|
uses: 'nwtgck/actions-netlify@v3.0',
|
|
543
756
|
with: {
|
|
544
757
|
'publish-dir': h.publishDir || 'dist',
|
|
545
758
|
'production-branch': 'main',
|
|
546
759
|
'github-token': '${{ secrets.GITHUB_TOKEN }}',
|
|
547
|
-
'deploy-message':
|
|
760
|
+
'deploy-message': isPreview ? 'Preview Deploy – ${{ github.event.number }}' : 'Production Deploy – ${{ github.sha }}',
|
|
548
761
|
'enable-pull-request-comment': true,
|
|
549
762
|
'enable-commit-comment': true,
|
|
763
|
+
'production-deploy': !isPreview,
|
|
764
|
+
alias: isPreview ? 'preview-${{ github.event.number }}' : undefined,
|
|
550
765
|
},
|
|
551
766
|
env: {
|
|
552
767
|
NETLIFY_AUTH_TOKEN: '${{ secrets.NETLIFY_AUTH_TOKEN }}',
|
|
@@ -562,7 +777,11 @@ class WorkflowGenerator {
|
|
|
562
777
|
}
|
|
563
778
|
steps.push(
|
|
564
779
|
{ name: 'Setup Pages', uses: 'actions/configure-pages@v4' },
|
|
565
|
-
{
|
|
780
|
+
{
|
|
781
|
+
name: 'Upload Pages artifact',
|
|
782
|
+
uses: 'actions/upload-pages-artifact@v3',
|
|
783
|
+
with: { path: (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist' },
|
|
784
|
+
},
|
|
566
785
|
{ name: 'Deploy to GitHub Pages', id: 'deployment', uses: 'actions/deploy-pages@v4' },
|
|
567
786
|
);
|
|
568
787
|
break;
|
|
@@ -572,8 +791,16 @@ class WorkflowGenerator {
|
|
|
572
791
|
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
573
792
|
const awsBuildDir = (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist';
|
|
574
793
|
steps.push(
|
|
575
|
-
{
|
|
576
|
-
|
|
794
|
+
{
|
|
795
|
+
name: 'Configure AWS credentials',
|
|
796
|
+
uses: 'aws-actions/configure-aws-credentials@v4',
|
|
797
|
+
with: {
|
|
798
|
+
'aws-access-key-id': '${{ secrets.AWS_ACCESS_KEY_ID }}',
|
|
799
|
+
'aws-secret-access-key': '${{ secrets.AWS_SECRET_ACCESS_KEY }}',
|
|
800
|
+
'aws-region': '${{ secrets.AWS_REGION }}',
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
{ name: 'Sync to S3', run: `aws s3 sync ./${awsBuildDir} s3://\${{ secrets.AWS_S3_BUCKET }} --delete` },
|
|
577
804
|
{ name: 'Invalidate CloudFront', run: 'aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"' },
|
|
578
805
|
);
|
|
579
806
|
break;
|
|
@@ -590,7 +817,15 @@ class WorkflowGenerator {
|
|
|
590
817
|
|
|
591
818
|
case 'Heroku': {
|
|
592
819
|
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript) });
|
|
593
|
-
steps.push({
|
|
820
|
+
steps.push({
|
|
821
|
+
name: 'Deploy to Heroku',
|
|
822
|
+
uses: 'akhileshns/heroku-deploy@v3.13.15',
|
|
823
|
+
with: {
|
|
824
|
+
heroku_api_key: '${{ secrets.HEROKU_API_KEY }}',
|
|
825
|
+
heroku_app_name: '${{ secrets.HEROKU_APP_NAME }}',
|
|
826
|
+
heroku_email: '${{ secrets.HEROKU_EMAIL }}',
|
|
827
|
+
},
|
|
828
|
+
});
|
|
594
829
|
break;
|
|
595
830
|
}
|
|
596
831
|
|
|
@@ -609,7 +844,15 @@ class WorkflowGenerator {
|
|
|
609
844
|
|
|
610
845
|
case 'Azure': {
|
|
611
846
|
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript) });
|
|
612
|
-
steps.push({
|
|
847
|
+
steps.push({
|
|
848
|
+
name: 'Deploy to Azure Web App',
|
|
849
|
+
uses: 'azure/webapps-deploy@v3',
|
|
850
|
+
with: {
|
|
851
|
+
'app-name': '${{ secrets.AZURE_APP_NAME }}',
|
|
852
|
+
'publish-profile': '${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}',
|
|
853
|
+
package: (this.frameworks[0] && this.frameworks[0].buildDir) || '.',
|
|
854
|
+
},
|
|
855
|
+
});
|
|
613
856
|
break;
|
|
614
857
|
}
|
|
615
858
|
|
|
@@ -620,13 +863,43 @@ class WorkflowGenerator {
|
|
|
620
863
|
return steps;
|
|
621
864
|
}
|
|
622
865
|
|
|
866
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
867
|
+
// Env comment block
|
|
868
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
869
|
+
|
|
870
|
+
_envComment() {
|
|
871
|
+
const { secrets, public: pub, sourceFile } = this.envVars;
|
|
872
|
+
if (!sourceFile || (secrets.length === 0 && pub.length === 0)) return '';
|
|
873
|
+
|
|
874
|
+
const lines = ['# Environment variables detected from ' + sourceFile + ':'];
|
|
875
|
+
if (secrets.length > 0) {
|
|
876
|
+
lines.push('# Secrets (add to GitHub -> Settings -> Secrets -> Actions):');
|
|
877
|
+
for (const s of secrets) lines.push('# ${{ secrets.' + s + ' }}');
|
|
878
|
+
}
|
|
879
|
+
if (pub.length > 0) {
|
|
880
|
+
lines.push('# Public vars:');
|
|
881
|
+
for (const p of pub) lines.push('# ' + p);
|
|
882
|
+
}
|
|
883
|
+
return lines.join('\n') + '\n';
|
|
884
|
+
}
|
|
885
|
+
|
|
623
886
|
// ══════════════════════════════════════════════════════════════════════════
|
|
624
887
|
// Utility helpers
|
|
625
888
|
// ══════════════════════════════════════════════════════════════════════════
|
|
626
889
|
|
|
890
|
+
_langForPackage(pkg) {
|
|
891
|
+
if (!pkg || !pkg.packageJson) return this.primaryLang;
|
|
892
|
+
// If the workspace has its own config, pick up its package manager
|
|
893
|
+
const wsPkg = pkg.packageJson;
|
|
894
|
+
const lang = { ...this.primaryLang };
|
|
895
|
+
if (wsPkg.engines && wsPkg.engines.node) {
|
|
896
|
+
const match = wsPkg.engines.node.match(/(\d+)/);
|
|
897
|
+
if (match) lang.nodeVersion = match[1];
|
|
898
|
+
}
|
|
899
|
+
return lang;
|
|
900
|
+
}
|
|
901
|
+
|
|
627
902
|
_findScript(names) {
|
|
628
|
-
const pkg = this.languages[0];
|
|
629
|
-
// Access from the raw packageJson in projectPath
|
|
630
903
|
const fs = require('fs');
|
|
631
904
|
const path = require('path');
|
|
632
905
|
try {
|
|
@@ -655,6 +928,10 @@ class WorkflowGenerator {
|
|
|
655
928
|
return map[langName] || 'javascript';
|
|
656
929
|
}
|
|
657
930
|
|
|
931
|
+
_slugify(name) {
|
|
932
|
+
return name.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').toLowerCase();
|
|
933
|
+
}
|
|
934
|
+
|
|
658
935
|
_toYaml(obj, header = '') {
|
|
659
936
|
const raw = yaml.dump(obj, {
|
|
660
937
|
indent: 2,
|