cistack 3.2.0 → 4.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/.github/workflows/ci.yml +70 -0
- package/README.md +202 -130
- package/bin/ciflow.js +1 -1
- package/index.d.ts +90 -0
- package/package.json +4 -2
- package/src/analyzers/codebase.js +43 -6
- package/src/analyzers/monorepo.js +7 -7
- package/src/analyzers/workflow.js +10 -2
- package/src/config/loader.js +65 -10
- package/src/detectors/framework.js +29 -25
- package/src/detectors/hosting.js +25 -10
- package/src/detectors/language.js +2 -2
- package/src/detectors/release.js +29 -8
- package/src/detectors/testing.js +18 -2
- package/src/generators/dependabot.js +24 -3
- package/src/generators/release.js +25 -10
- package/src/generators/workflow.js +316 -89
- package/src/index.js +21 -16
- package/src/utils/helpers.js +21 -7
- package/tests/run.js +808 -0
|
@@ -20,7 +20,9 @@ class WorkflowGenerator {
|
|
|
20
20
|
this.projectPath = projectPath;
|
|
21
21
|
this.envVars = config.envVars || { secrets: [], public: [], all: [], sourceFile: null };
|
|
22
22
|
this.monorepoPackages = config.monorepoPackages || [];
|
|
23
|
+
this.lockFiles = new Set(config.lockFiles || []);
|
|
23
24
|
this.extraConfig = config._config || {}; // raw cistack.config.js
|
|
25
|
+
this.defaultBranch = config.defaultBranch || config.currentBranch || null;
|
|
24
26
|
|
|
25
27
|
// Convenient accessors
|
|
26
28
|
this.primaryLang = this.languages[0] || { name: 'JavaScript', packageManager: 'npm', nodeVersion: '20' };
|
|
@@ -35,6 +37,41 @@ class WorkflowGenerator {
|
|
|
35
37
|
(this.extraConfig.monorepo && this.extraConfig.monorepo.perPackage) ||
|
|
36
38
|
this.monorepoPackages.length > 1
|
|
37
39
|
);
|
|
40
|
+
|
|
41
|
+
// Initial runtime detection
|
|
42
|
+
this._detectRuntimeVersions();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_detectRuntimeVersions() {
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
|
|
49
|
+
// 1. Node.js
|
|
50
|
+
if (!this.primaryLang.nodeVersion) {
|
|
51
|
+
const nvmrcPath = path.join(this.projectPath, '.nvmrc');
|
|
52
|
+
if (fs.existsSync(nvmrcPath)) {
|
|
53
|
+
this.primaryLang.nodeVersion = fs.readFileSync(nvmrcPath, 'utf8').trim().replace('v', '');
|
|
54
|
+
} else {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(this.projectPath, 'package.json'), 'utf8'));
|
|
57
|
+
if (pkg.engines && pkg.engines.node) {
|
|
58
|
+
const match = pkg.engines.node.match(/(\d+)/);
|
|
59
|
+
if (match) this.primaryLang.nodeVersion = match[1];
|
|
60
|
+
}
|
|
61
|
+
} catch (_) {}
|
|
62
|
+
}
|
|
63
|
+
this.primaryLang.nodeVersion = this.primaryLang.nodeVersion || '20';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Python
|
|
67
|
+
if (this.primaryLang.name === 'Python' && !this.primaryLang.pythonVersion) {
|
|
68
|
+
const pythonVersionPath = path.join(this.projectPath, '.python-version');
|
|
69
|
+
if (fs.existsSync(pythonVersionPath)) {
|
|
70
|
+
this.primaryLang.pythonVersion = fs.readFileSync(pythonVersionPath, 'utf8').trim();
|
|
71
|
+
} else {
|
|
72
|
+
this.primaryLang.pythonVersion = '3.11';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
}
|
|
39
76
|
|
|
40
77
|
generate() {
|
|
@@ -94,7 +131,7 @@ class WorkflowGenerator {
|
|
|
94
131
|
const lang = this._langForPackage(pkg);
|
|
95
132
|
const jobs = {};
|
|
96
133
|
|
|
97
|
-
const branches = this.
|
|
134
|
+
const branches = this._resolveBranches(['main', 'master', 'develop']);
|
|
98
135
|
|
|
99
136
|
// ── lint job ──────────────────────────────────────────────────────────
|
|
100
137
|
jobs.lint = {
|
|
@@ -104,8 +141,12 @@ class WorkflowGenerator {
|
|
|
104
141
|
this._stepCheckout(),
|
|
105
142
|
...this._setupSteps(lang),
|
|
106
143
|
this._stepInstallDeps(lang),
|
|
107
|
-
this._stepLint(lang),
|
|
144
|
+
this._stepLint(lang, pkg),
|
|
108
145
|
].filter(Boolean),
|
|
146
|
+
env: {
|
|
147
|
+
CI: 'true',
|
|
148
|
+
...this._getPublicEnv(),
|
|
149
|
+
},
|
|
109
150
|
};
|
|
110
151
|
|
|
111
152
|
// ── test job ──────────────────────────────────────────────────────────
|
|
@@ -117,16 +158,20 @@ class WorkflowGenerator {
|
|
|
117
158
|
...(testMatrix ? { strategy: testMatrix } : {}),
|
|
118
159
|
steps: [
|
|
119
160
|
this._stepCheckout(),
|
|
120
|
-
...this._setupSteps(lang),
|
|
161
|
+
...this._setupSteps(lang, !!testMatrix),
|
|
121
162
|
this._stepInstallDeps(lang),
|
|
122
|
-
...this._unitTestSteps(lang),
|
|
163
|
+
...this._unitTestSteps(lang, pkg),
|
|
123
164
|
this._stepUploadCoverage(),
|
|
124
165
|
].filter(Boolean),
|
|
166
|
+
env: {
|
|
167
|
+
CI: 'true',
|
|
168
|
+
...this._getPublicEnv(),
|
|
169
|
+
},
|
|
125
170
|
};
|
|
126
171
|
}
|
|
127
172
|
|
|
128
173
|
// ── build job ─────────────────────────────────────────────────────────
|
|
129
|
-
const buildSteps = this._buildSteps(lang);
|
|
174
|
+
const buildSteps = this._buildSteps(lang, pkg);
|
|
130
175
|
if (buildSteps.length > 0) {
|
|
131
176
|
jobs.build = {
|
|
132
177
|
name: '🏗️ Build',
|
|
@@ -137,18 +182,46 @@ class WorkflowGenerator {
|
|
|
137
182
|
...this._setupSteps(lang),
|
|
138
183
|
this._stepInstallDeps(lang),
|
|
139
184
|
...buildSteps,
|
|
140
|
-
this._stepUploadArtifact(),
|
|
185
|
+
this._stepUploadArtifact(lang),
|
|
141
186
|
].filter(Boolean),
|
|
187
|
+
env: {
|
|
188
|
+
NODE_ENV: 'production',
|
|
189
|
+
...this._getPublicEnv(),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── lighthouse job ──────────────────────────────────────────────────
|
|
195
|
+
if (jobs.build && this.frameworks.some(f => ['Next.js', 'React', 'Vue', 'Svelte', 'Nuxt'].includes(f.name))) {
|
|
196
|
+
jobs.lighthouse = {
|
|
197
|
+
name: '⚡ Lighthouse Audit',
|
|
198
|
+
'runs-on': 'ubuntu-latest',
|
|
199
|
+
needs: ['build'],
|
|
200
|
+
if: "github.event_name == 'pull_request'",
|
|
201
|
+
steps: [
|
|
202
|
+
this._stepCheckout(),
|
|
203
|
+
{
|
|
204
|
+
name: 'Run Lighthouse on build output',
|
|
205
|
+
uses: 'treosh/lighthouse-ci-action@v11',
|
|
206
|
+
with: {
|
|
207
|
+
uploadArtifacts: true,
|
|
208
|
+
temporaryPublicStorage: true,
|
|
209
|
+
configPath: './.lighthouserc.json',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
'continue-on-error': true,
|
|
142
214
|
};
|
|
143
215
|
}
|
|
144
216
|
|
|
145
217
|
// ── e2e job ───────────────────────────────────────────────────────────
|
|
146
218
|
if (this.e2eTests.length > 0) {
|
|
147
219
|
const e2eTest = this.e2eTests[0];
|
|
220
|
+
const e2eNeeds = jobs.build ? ['build'] : ['lint', ...(jobs.test ? ['test'] : [])];
|
|
148
221
|
jobs.e2e = {
|
|
149
222
|
name: '🎭 E2E Tests',
|
|
150
223
|
'runs-on': 'ubuntu-latest',
|
|
151
|
-
needs:
|
|
224
|
+
needs: e2eNeeds,
|
|
152
225
|
steps: [
|
|
153
226
|
this._stepCheckout(),
|
|
154
227
|
...this._setupSteps(lang),
|
|
@@ -191,9 +264,13 @@ class WorkflowGenerator {
|
|
|
191
264
|
};
|
|
192
265
|
|
|
193
266
|
const envComment = this._envComment();
|
|
267
|
+
const pipelineStages = ['lint'];
|
|
268
|
+
if (jobs.test) pipelineStages.push('test');
|
|
269
|
+
if (jobs.build) pipelineStages.push('build');
|
|
270
|
+
if (jobs.e2e) pipelineStages.push('e2e');
|
|
194
271
|
const header =
|
|
195
272
|
`# Generated by cistack v${version} — https://github.com/cistack\n` +
|
|
196
|
-
`# CI Pipeline:
|
|
273
|
+
`# CI Pipeline: ${pipelineStages.join(' → ')}\n` +
|
|
197
274
|
envComment +
|
|
198
275
|
`\n`;
|
|
199
276
|
|
|
@@ -203,8 +280,18 @@ class WorkflowGenerator {
|
|
|
203
280
|
// ── Monorepo root CI (matrix over all workspaces) ────────────────────────
|
|
204
281
|
_buildMonorepoRootCI() {
|
|
205
282
|
const lang = this.primaryLang;
|
|
206
|
-
const branches = this.
|
|
207
|
-
const
|
|
283
|
+
const branches = this._resolveBranches(['main', 'master', 'develop']);
|
|
284
|
+
const matrixEntries = this.monorepoPackages.map((pkg) => {
|
|
285
|
+
const pkgScripts = (pkg.packageJson && pkg.packageJson.scripts) || {};
|
|
286
|
+
return {
|
|
287
|
+
name: pkg.name,
|
|
288
|
+
package: pkg.relativePath,
|
|
289
|
+
lintScript: this._findScript(['lint', 'lint:ci', 'eslint'], pkg) || '',
|
|
290
|
+
testScript: (pkgScripts['test:ci'] && 'test:ci') || (pkgScripts.test && 'test') || '',
|
|
291
|
+
buildScript: this._findScript(['build', 'build:prod', 'compile'], pkg) || '',
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
const buildablePackages = matrixEntries.filter((pkg) => pkg.buildScript);
|
|
208
295
|
|
|
209
296
|
const workflow = {
|
|
210
297
|
name: 'CI — Monorepo',
|
|
@@ -222,50 +309,71 @@ class WorkflowGenerator {
|
|
|
222
309
|
'runs-on': 'ubuntu-latest',
|
|
223
310
|
strategy: {
|
|
224
311
|
matrix: {
|
|
225
|
-
|
|
312
|
+
include: matrixEntries,
|
|
226
313
|
},
|
|
227
314
|
'fail-fast': false,
|
|
228
315
|
},
|
|
229
316
|
steps: [
|
|
230
317
|
this._stepCheckout(),
|
|
231
318
|
...this._setupSteps(lang),
|
|
232
|
-
|
|
233
|
-
name: 'Install dependencies',
|
|
234
|
-
run: lang.packageManager === 'pnpm'
|
|
235
|
-
? 'pnpm --filter ${{ matrix.package }} install --frozen-lockfile'
|
|
236
|
-
: lang.packageManager === 'yarn'
|
|
237
|
-
? 'yarn workspace ${{ matrix.package }} install'
|
|
238
|
-
: 'npm ci --workspace=${{ matrix.package }}',
|
|
239
|
-
},
|
|
319
|
+
this._stepInstallDeps(lang),
|
|
240
320
|
{
|
|
241
321
|
name: 'Lint',
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: lang.packageManager === 'yarn'
|
|
245
|
-
? 'yarn workspace ${{ matrix.package }} run lint || true'
|
|
246
|
-
: 'npm run --workspace=${{ matrix.package }} lint || true',
|
|
322
|
+
if: '${{ matrix.lintScript != \'\' }}',
|
|
323
|
+
run: this._workspaceRunCommand(lang, '${{ matrix.lintScript }}'),
|
|
247
324
|
},
|
|
248
325
|
{
|
|
249
326
|
name: 'Test',
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
: lang.packageManager === 'yarn'
|
|
253
|
-
? 'yarn workspace ${{ matrix.package }} run test || true'
|
|
254
|
-
: 'npm run --workspace=${{ matrix.package }} test || true',
|
|
327
|
+
if: '${{ matrix.testScript != \'\' }}',
|
|
328
|
+
run: this._workspaceRunCommand(lang, '${{ matrix.testScript }}'),
|
|
255
329
|
},
|
|
256
330
|
{
|
|
257
331
|
name: 'Build',
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
: lang.packageManager === 'yarn'
|
|
261
|
-
? 'yarn workspace ${{ matrix.package }} run build || true'
|
|
262
|
-
: 'npm run --workspace=${{ matrix.package }} build || true',
|
|
332
|
+
if: '${{ matrix.buildScript != \'\' }}',
|
|
333
|
+
run: this._workspaceRunCommand(lang, '${{ matrix.buildScript }}'),
|
|
263
334
|
},
|
|
264
335
|
].filter(Boolean),
|
|
336
|
+
env: {
|
|
337
|
+
NODE_ENV: 'test',
|
|
338
|
+
CI: 'true',
|
|
339
|
+
...this._getPublicEnv(),
|
|
340
|
+
},
|
|
265
341
|
},
|
|
266
342
|
},
|
|
267
343
|
};
|
|
268
344
|
|
|
345
|
+
if (buildablePackages.length > 0 && this.frameworks.some(f => ['Next.js', 'React', 'Vue', 'Svelte', 'Nuxt'].includes(f.name))) {
|
|
346
|
+
workflow.jobs.lighthouse = {
|
|
347
|
+
name: '⚡ Lighthouse (Root)',
|
|
348
|
+
'runs-on': 'ubuntu-latest',
|
|
349
|
+
strategy: {
|
|
350
|
+
matrix: {
|
|
351
|
+
include: buildablePackages,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
steps: [
|
|
355
|
+
this._stepCheckout(),
|
|
356
|
+
...this._setupSteps(lang),
|
|
357
|
+
{
|
|
358
|
+
...this._stepInstallDeps(lang),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'Build workspace',
|
|
362
|
+
run: this._workspaceRunCommand(lang, '${{ matrix.buildScript }}'),
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'Lighthouse',
|
|
366
|
+
uses: 'treosh/lighthouse-ci-action@v11',
|
|
367
|
+
with: {
|
|
368
|
+
uploadArtifacts: true,
|
|
369
|
+
temporaryPublicStorage: true,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
'continue-on-error': true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
269
377
|
return this._toYaml(
|
|
270
378
|
workflow,
|
|
271
379
|
`# Generated by cistack v${version} — https://github.com/cistack\n# Monorepo CI — matrix over all workspaces\n\n`
|
|
@@ -279,8 +387,11 @@ class WorkflowGenerator {
|
|
|
279
387
|
_buildDeployWorkflow() {
|
|
280
388
|
const h = this.primaryHosting;
|
|
281
389
|
const lang = this.primaryLang;
|
|
282
|
-
const branches = this.
|
|
390
|
+
const branches = this._resolveBranches(['main', 'master']);
|
|
391
|
+
const productionBranches = branches.filter((b) => b !== 'develop');
|
|
392
|
+
const deployBranches = productionBranches.length > 0 ? productionBranches : branches;
|
|
283
393
|
const isGHPages = h.name === 'GitHub Pages';
|
|
394
|
+
const supportsPreview = ['Firebase', 'Vercel', 'Netlify'].includes(h.name);
|
|
284
395
|
|
|
285
396
|
const preDeploySteps = [
|
|
286
397
|
this._stepCheckout(),
|
|
@@ -288,9 +399,10 @@ class WorkflowGenerator {
|
|
|
288
399
|
this._stepInstallDeps(lang),
|
|
289
400
|
].filter(Boolean);
|
|
290
401
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
402
|
+
const primaryDeployBranch = deployBranches[0] || this.defaultBranch || 'main';
|
|
403
|
+
const deploySteps = this._hostingDeploySteps(h, lang, false, primaryDeployBranch); // production
|
|
404
|
+
// Only generate PR preview steps for platforms that natively isolate them
|
|
405
|
+
const previewSteps = supportsPreview ? this._hostingDeploySteps(h, lang, true, primaryDeployBranch) : [];
|
|
294
406
|
|
|
295
407
|
// GitHub Pages requires special permissions on the deploy job
|
|
296
408
|
const ghPagesPermissions = isGHPages
|
|
@@ -315,7 +427,27 @@ class WorkflowGenerator {
|
|
|
315
427
|
if: "github.event_name == 'pull_request'",
|
|
316
428
|
'runs-on': 'ubuntu-latest',
|
|
317
429
|
environment: 'preview',
|
|
318
|
-
steps: [
|
|
430
|
+
steps: [
|
|
431
|
+
...preDeploySteps,
|
|
432
|
+
...previewSteps,
|
|
433
|
+
{
|
|
434
|
+
name: 'Comment PR',
|
|
435
|
+
if: 'always()',
|
|
436
|
+
uses: 'actions/github-script@v7',
|
|
437
|
+
with: {
|
|
438
|
+
script: `
|
|
439
|
+
const deploymentUrl = process.env.DEPLOYMENT_URL;
|
|
440
|
+
if (!deploymentUrl) return;
|
|
441
|
+
github.rest.issues.createComment({
|
|
442
|
+
issue_number: context.issue.number,
|
|
443
|
+
owner: context.repo.owner,
|
|
444
|
+
repo: context.repo.repo,
|
|
445
|
+
body: '🚀 **Deployment Preview Ready!**\\n\\n[View Preview](' + deploymentUrl + ')'
|
|
446
|
+
});
|
|
447
|
+
`
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
].filter(Boolean),
|
|
319
451
|
};
|
|
320
452
|
}
|
|
321
453
|
|
|
@@ -331,14 +463,18 @@ class WorkflowGenerator {
|
|
|
331
463
|
|
|
332
464
|
const envComment = this._envComment();
|
|
333
465
|
|
|
334
|
-
//
|
|
335
|
-
const onTrigger =
|
|
336
|
-
? { push: { branches:
|
|
337
|
-
: { push: { branches:
|
|
466
|
+
// Only trigger on PR if the platform supports preview deployments
|
|
467
|
+
const onTrigger = supportsPreview
|
|
468
|
+
? { push: { branches: deployBranches }, pull_request: { branches }, workflow_dispatch: {} }
|
|
469
|
+
: { push: { branches: deployBranches }, workflow_dispatch: {} };
|
|
338
470
|
|
|
339
471
|
const workflow = {
|
|
340
472
|
name: `Deploy to ${h.name}`,
|
|
341
473
|
on: onTrigger,
|
|
474
|
+
concurrency: {
|
|
475
|
+
group: '${{ github.workflow }}-${{ github.ref }}',
|
|
476
|
+
'cancel-in-progress': true,
|
|
477
|
+
},
|
|
342
478
|
jobs,
|
|
343
479
|
};
|
|
344
480
|
|
|
@@ -353,14 +489,15 @@ class WorkflowGenerator {
|
|
|
353
489
|
// ══════════════════════════════════════════════════════════════════════════
|
|
354
490
|
|
|
355
491
|
_buildDockerWorkflow() {
|
|
492
|
+
const branches = this._resolveBranches(['main', 'master']);
|
|
356
493
|
const workflow = {
|
|
357
494
|
name: 'Docker Build & Push',
|
|
358
495
|
on: {
|
|
359
496
|
push: {
|
|
360
|
-
branches
|
|
497
|
+
branches,
|
|
361
498
|
tags: ['v*.*.*'],
|
|
362
499
|
},
|
|
363
|
-
pull_request: { branches
|
|
500
|
+
pull_request: { branches },
|
|
364
501
|
},
|
|
365
502
|
env: {
|
|
366
503
|
REGISTRY: 'ghcr.io',
|
|
@@ -431,6 +568,7 @@ class WorkflowGenerator {
|
|
|
431
568
|
|
|
432
569
|
_buildSecurityWorkflow() {
|
|
433
570
|
const lang = this.primaryLang;
|
|
571
|
+
const branches = this._resolveBranches(['main', 'master']);
|
|
434
572
|
const steps = [this._stepCheckout()];
|
|
435
573
|
|
|
436
574
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
@@ -450,7 +588,7 @@ class WorkflowGenerator {
|
|
|
450
588
|
|
|
451
589
|
if (lang.name === 'Python') {
|
|
452
590
|
steps.push(
|
|
453
|
-
{ name: 'Set up Python', uses: 'actions/setup-python@v5', with: { 'python-version': '3.
|
|
591
|
+
{ name: 'Set up Python', uses: 'actions/setup-python@v5', with: { 'python-version': lang.pythonVersion || '3.11' } },
|
|
454
592
|
{ name: 'Install safety', run: 'pip install safety' },
|
|
455
593
|
{ name: 'Run safety check', run: 'safety check' },
|
|
456
594
|
);
|
|
@@ -476,19 +614,19 @@ class WorkflowGenerator {
|
|
|
476
614
|
const workflow = {
|
|
477
615
|
name: 'Security Audit',
|
|
478
616
|
on: {
|
|
479
|
-
push: { branches
|
|
480
|
-
pull_request: { branches
|
|
617
|
+
push: { branches },
|
|
618
|
+
pull_request: { branches },
|
|
481
619
|
schedule: [{ cron: '0 6 * * 1' }],
|
|
482
620
|
},
|
|
621
|
+
permissions: {
|
|
622
|
+
actions: 'read',
|
|
623
|
+
contents: 'read',
|
|
624
|
+
'security-events': 'write',
|
|
625
|
+
},
|
|
483
626
|
jobs: {
|
|
484
627
|
security: {
|
|
485
628
|
name: '🔒 Security Audit',
|
|
486
629
|
'runs-on': 'ubuntu-latest',
|
|
487
|
-
permissions: {
|
|
488
|
-
actions: 'read',
|
|
489
|
-
contents: 'read',
|
|
490
|
-
'security-events': 'write',
|
|
491
|
-
},
|
|
492
630
|
steps: steps.filter(Boolean),
|
|
493
631
|
},
|
|
494
632
|
},
|
|
@@ -509,12 +647,19 @@ class WorkflowGenerator {
|
|
|
509
647
|
* Returns setup + cache steps for the given language.
|
|
510
648
|
* v2.0.0: added explicit caching for pip, poetry, cargo, maven, gradle, bundler, go, composer.
|
|
511
649
|
*/
|
|
512
|
-
_setupSteps(lang) {
|
|
650
|
+
_setupSteps(lang, useMatrix = false) {
|
|
513
651
|
const steps = [];
|
|
514
652
|
const cacheOverride = this.extraConfig.cache || {};
|
|
515
653
|
|
|
516
654
|
// ── JavaScript / TypeScript ──────────────────────────────────────────
|
|
517
655
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
656
|
+
if (lang.packageManager === 'bun') {
|
|
657
|
+
steps.push({
|
|
658
|
+
name: 'Set up Bun',
|
|
659
|
+
uses: 'oven-sh/setup-bun@v2',
|
|
660
|
+
with: { 'bun-version': 'latest' },
|
|
661
|
+
});
|
|
662
|
+
}
|
|
518
663
|
if (lang.packageManager === 'pnpm') {
|
|
519
664
|
steps.push({ name: 'Install pnpm', uses: 'pnpm/action-setup@v3', with: { version: 'latest' } });
|
|
520
665
|
}
|
|
@@ -522,9 +667,17 @@ class WorkflowGenerator {
|
|
|
522
667
|
name: 'Set up Node.js',
|
|
523
668
|
uses: 'actions/setup-node@v4',
|
|
524
669
|
with: {
|
|
525
|
-
'node-version': lang.nodeVersion || '20',
|
|
670
|
+
'node-version': useMatrix ? '${{ matrix.node-version }}' : (lang.nodeVersion || '20'),
|
|
526
671
|
// Use native caching in setup-node
|
|
527
|
-
cache: cacheOverride.npm !== false
|
|
672
|
+
cache: cacheOverride.npm !== false
|
|
673
|
+
? (lang.packageManager === 'yarn'
|
|
674
|
+
? 'yarn'
|
|
675
|
+
: lang.packageManager === 'pnpm'
|
|
676
|
+
? 'pnpm'
|
|
677
|
+
: lang.packageManager === 'npm'
|
|
678
|
+
? 'npm'
|
|
679
|
+
: undefined)
|
|
680
|
+
: undefined,
|
|
528
681
|
},
|
|
529
682
|
});
|
|
530
683
|
}
|
|
@@ -535,7 +688,7 @@ class WorkflowGenerator {
|
|
|
535
688
|
name: 'Set up Python',
|
|
536
689
|
uses: 'actions/setup-python@v5',
|
|
537
690
|
with: {
|
|
538
|
-
'python-version': '3.
|
|
691
|
+
'python-version': useMatrix ? '${{ matrix.python-version }}' : (lang.pythonVersion || '3.11'),
|
|
539
692
|
// Native caching for pip/poetry
|
|
540
693
|
cache: cacheOverride.pip !== false ? (lang.packageManager === 'poetry' ? 'poetry' : 'pip') : undefined
|
|
541
694
|
},
|
|
@@ -623,9 +776,9 @@ class WorkflowGenerator {
|
|
|
623
776
|
|
|
624
777
|
_stepInstallDeps(lang) {
|
|
625
778
|
const pm = lang.packageManager;
|
|
626
|
-
if (pm === 'npm') return { name: 'Install dependencies', run: 'npm ci' };
|
|
627
|
-
if (pm === 'yarn') return { name: 'Install dependencies', run: 'yarn install --frozen-lockfile' };
|
|
628
|
-
if (pm === 'pnpm') return { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' };
|
|
779
|
+
if (pm === 'npm') return { name: 'Install dependencies', run: this.lockFiles.has('package-lock.json') ? 'npm ci' : 'npm install' };
|
|
780
|
+
if (pm === 'yarn') return { name: 'Install dependencies', run: this.lockFiles.has('yarn.lock') ? 'yarn install --frozen-lockfile' : 'yarn install' };
|
|
781
|
+
if (pm === 'pnpm') return { name: 'Install dependencies', run: this.lockFiles.has('pnpm-lock.yaml') ? 'pnpm install --frozen-lockfile' : 'pnpm install' };
|
|
629
782
|
if (pm === 'bun') return { name: 'Install dependencies', run: 'bun install' };
|
|
630
783
|
if (pm === 'pip') return { name: 'Install dependencies', run: 'pip install -r requirements.txt' };
|
|
631
784
|
if (pm === 'poetry') return { name: 'Install dependencies', run: 'pip install poetry && poetry install' };
|
|
@@ -639,46 +792,50 @@ class WorkflowGenerator {
|
|
|
639
792
|
return { name: 'Install dependencies', run: 'npm ci' };
|
|
640
793
|
}
|
|
641
794
|
|
|
642
|
-
_stepLint(lang) {
|
|
643
|
-
const pm = lang.packageManager || 'npm';
|
|
644
|
-
|
|
795
|
+
_stepLint(lang, pkg = null) {
|
|
645
796
|
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
646
|
-
const lintScript = this._findScript(['lint', 'lint:ci', 'eslint']);
|
|
647
|
-
const typeCheck = this._findScript(['type-check', 'typecheck', 'tsc']);
|
|
648
|
-
const format = this._findScript(['format:check', 'prettier:check', 'fmt:check']);
|
|
649
|
-
const runPfx = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run';
|
|
797
|
+
const lintScript = this._findScript(['lint', 'lint:ci', 'eslint'], pkg);
|
|
798
|
+
const typeCheck = this._findScript(['type-check', 'typecheck', 'tsc'], pkg);
|
|
799
|
+
const format = this._findScript(['format:check', 'prettier:check', 'fmt:check'], pkg);
|
|
650
800
|
|
|
651
801
|
const cmds = [];
|
|
652
|
-
if (lintScript) cmds.push(
|
|
653
|
-
if (typeCheck) cmds.push(
|
|
654
|
-
if (format) cmds.push(
|
|
655
|
-
if (cmds.length === 0)
|
|
802
|
+
if (lintScript) cmds.push(this._scriptCommand(lang, lintScript, pkg));
|
|
803
|
+
if (typeCheck) cmds.push(this._scriptCommand(lang, typeCheck, pkg));
|
|
804
|
+
if (format) cmds.push(this._scriptCommand(lang, format, pkg));
|
|
805
|
+
if (cmds.length === 0) {
|
|
806
|
+
const lintTarget = pkg ? pkg.relativePath : '.';
|
|
807
|
+
cmds.push(`npx eslint ${lintTarget} --ext .js,.jsx,.ts,.tsx --max-warnings 0`);
|
|
808
|
+
}
|
|
656
809
|
|
|
657
810
|
return { name: 'Lint', run: cmds.join('\n') };
|
|
658
811
|
}
|
|
659
812
|
|
|
660
|
-
if (lang.name === 'Python') return { name: 'Lint', run: 'pip install flake8 && flake8 .' };
|
|
813
|
+
if (lang.name === 'Python') return { name: 'Lint', run: 'pip install flake8 black && flake8 . && black --check .' };
|
|
661
814
|
if (lang.name === 'Go') return { name: 'Lint', run: 'gofmt -d . && go vet ./...' };
|
|
662
815
|
if (lang.name === 'Rust') return { name: 'Lint', run: 'cargo clippy -- -D warnings && cargo fmt --check' };
|
|
663
816
|
if (lang.name === 'Ruby') return { name: 'Lint', run: 'gem install rubocop && rubocop' };
|
|
664
|
-
if (lang.name === 'PHP') return { name: 'Lint', run: 'vendor/bin/phpcs' };
|
|
817
|
+
if (lang.name === 'PHP') return { name: 'Lint', run: 'vendor/bin/phpcs && vendor/bin/phpstan analyze' };
|
|
665
818
|
|
|
666
819
|
return null;
|
|
667
820
|
}
|
|
668
821
|
|
|
669
|
-
_unitTestSteps(lang) {
|
|
822
|
+
_unitTestSteps(lang, pkg = null) {
|
|
823
|
+
if (pkg && ['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
824
|
+
const pkgTestScript = this._findScript(['test:ci', 'test'], pkg);
|
|
825
|
+
if (pkgTestScript) {
|
|
826
|
+
return [{ name: 'Run tests', run: this._scriptCommand(lang, pkgTestScript, pkg) }];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
670
829
|
return this.unitTests.map((t) => ({ name: `Run ${t.name}`, run: t.command }));
|
|
671
830
|
}
|
|
672
831
|
|
|
673
|
-
_buildSteps(lang) {
|
|
674
|
-
const
|
|
675
|
-
const buildScript = this._findScript(['build', 'build:prod']);
|
|
832
|
+
_buildSteps(lang, pkg = null) {
|
|
833
|
+
const buildScript = this._findScript(['build', 'build:prod', 'compile'], pkg);
|
|
676
834
|
if (!buildScript && !['Go', 'Rust', 'Java', 'Kotlin'].includes(lang.name)) return [];
|
|
677
835
|
|
|
678
836
|
const steps = [];
|
|
679
837
|
if (['JavaScript', 'TypeScript'].includes(lang.name) && buildScript) {
|
|
680
|
-
|
|
681
|
-
steps.push({ name: 'Build', run: `${runPfx} run ${buildScript}`, env: { NODE_ENV: 'production' } });
|
|
838
|
+
steps.push({ name: 'Build', run: this._scriptCommand(lang, buildScript, pkg), env: { NODE_ENV: 'production' } });
|
|
682
839
|
}
|
|
683
840
|
if (lang.name === 'Go') steps.push({ name: 'Build', run: 'go build -v ./...' });
|
|
684
841
|
if (lang.name === 'Rust') steps.push({ name: 'Build', run: 'cargo build --release' });
|
|
@@ -688,8 +845,21 @@ class WorkflowGenerator {
|
|
|
688
845
|
return steps;
|
|
689
846
|
}
|
|
690
847
|
|
|
691
|
-
_stepUploadArtifact() {
|
|
692
|
-
|
|
848
|
+
_stepUploadArtifact(lang) {
|
|
849
|
+
let buildDir = null;
|
|
850
|
+
|
|
851
|
+
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
852
|
+
buildDir = (this.frameworks[0] && this.frameworks[0].buildDir) || null;
|
|
853
|
+
} else if (lang.name === 'Rust') {
|
|
854
|
+
buildDir = 'target/release';
|
|
855
|
+
} else if (lang.name === 'Java') {
|
|
856
|
+
buildDir = 'target';
|
|
857
|
+
} else if (lang.name === 'Kotlin') {
|
|
858
|
+
buildDir = 'build/libs';
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!buildDir) return null;
|
|
862
|
+
|
|
693
863
|
return {
|
|
694
864
|
name: 'Upload build artifact',
|
|
695
865
|
uses: 'actions/upload-artifact@v4',
|
|
@@ -719,15 +889,48 @@ class WorkflowGenerator {
|
|
|
719
889
|
return null;
|
|
720
890
|
}
|
|
721
891
|
|
|
892
|
+
_resolveBranches(fallback) {
|
|
893
|
+
if (Array.isArray(this.extraConfig.branches) && this.extraConfig.branches.length > 0) {
|
|
894
|
+
return [...new Set(this.extraConfig.branches)];
|
|
895
|
+
}
|
|
896
|
+
if (this.defaultBranch) {
|
|
897
|
+
return [this.defaultBranch];
|
|
898
|
+
}
|
|
899
|
+
return fallback;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
_workspaceRunCommand(lang, scriptName) {
|
|
903
|
+
const pm = lang.packageManager || 'npm';
|
|
904
|
+
if (pm === 'pnpm') return `pnpm --filter \${{ matrix.package }} run ${scriptName}`;
|
|
905
|
+
if (pm === 'yarn') return `yarn workspace \${{ matrix.name }} run ${scriptName}`;
|
|
906
|
+
if (pm === 'bun') return `bun run --filter './\${{ matrix.package }}' ${scriptName}`;
|
|
907
|
+
return `npm run ${scriptName} --workspace=\${{ matrix.name }}`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
_scriptCommand(lang, scriptName, pkg = null) {
|
|
911
|
+
const pm = lang.packageManager || 'npm';
|
|
912
|
+
|
|
913
|
+
if (pkg) {
|
|
914
|
+
if (pm === 'pnpm') return `pnpm --filter ${pkg.relativePath} run ${scriptName}`;
|
|
915
|
+
if (pm === 'yarn') return `yarn workspace ${pkg.name} run ${scriptName}`;
|
|
916
|
+
if (pm === 'bun') return `bun run --filter './${pkg.relativePath}' ${scriptName}`;
|
|
917
|
+
return `npm run ${scriptName} --workspace=${pkg.name}`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (pm === 'yarn') return `yarn run ${scriptName}`;
|
|
921
|
+
if (pm === 'pnpm') return `pnpm run ${scriptName}`;
|
|
922
|
+
if (pm === 'bun') return `bun run ${scriptName}`;
|
|
923
|
+
return `npm run ${scriptName}`;
|
|
924
|
+
}
|
|
925
|
+
|
|
722
926
|
// ══════════════════════════════════════════════════════════════════════════
|
|
723
927
|
// Hosting-specific deploy steps
|
|
724
928
|
// ══════════════════════════════════════════════════════════════════════════
|
|
725
929
|
|
|
726
|
-
_hostingDeploySteps(h, lang, isPreview = false) {
|
|
930
|
+
_hostingDeploySteps(h, lang, isPreview = false, productionBranch = null) {
|
|
727
931
|
const steps = [];
|
|
728
932
|
const buildScript = this._findScript(['build', 'build:prod']);
|
|
729
|
-
const
|
|
730
|
-
const runCmd = (s) => `${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm'} run ${s}`;
|
|
933
|
+
const runCmd = (s) => this._scriptCommand(lang, s);
|
|
731
934
|
|
|
732
935
|
switch (h.name) {
|
|
733
936
|
case 'Firebase': {
|
|
@@ -767,7 +970,8 @@ class WorkflowGenerator {
|
|
|
767
970
|
},
|
|
768
971
|
{
|
|
769
972
|
name: 'Deploy to Vercel',
|
|
770
|
-
|
|
973
|
+
id: 'deploy',
|
|
974
|
+
run: `vercel deploy --prebuilt${prodFlag ? ' ' + prodFlag : ''} --token=\${{ secrets.VERCEL_TOKEN }} > deployment_url.txt && echo "DEPLOYMENT_URL=$(cat deployment_url.txt)" >> $GITHUB_ENV`,
|
|
771
975
|
env: vercelEnv,
|
|
772
976
|
},
|
|
773
977
|
);
|
|
@@ -780,10 +984,11 @@ class WorkflowGenerator {
|
|
|
780
984
|
}
|
|
781
985
|
steps.push({
|
|
782
986
|
name: isPreview ? 'Deploy Preview' : 'Deploy to Netlify',
|
|
987
|
+
id: 'netlify_deploy',
|
|
783
988
|
uses: 'nwtgck/actions-netlify@v3.0',
|
|
784
989
|
with: {
|
|
785
|
-
'publish-dir': h.publishDir || 'dist',
|
|
786
|
-
'production-branch': 'main',
|
|
990
|
+
'publish-dir': h.publishDir || (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist',
|
|
991
|
+
'production-branch': productionBranch || this.defaultBranch || 'main',
|
|
787
992
|
'github-token': '${{ secrets.GITHUB_TOKEN }}',
|
|
788
993
|
'deploy-message': isPreview ? 'Preview Deploy – ${{ github.event.number }}' : 'Production Deploy – ${{ github.sha }}',
|
|
789
994
|
'enable-pull-request-comment': true,
|
|
@@ -796,6 +1001,12 @@ class WorkflowGenerator {
|
|
|
796
1001
|
NETLIFY_SITE_ID: '${{ secrets.NETLIFY_SITE_ID }}',
|
|
797
1002
|
},
|
|
798
1003
|
});
|
|
1004
|
+
if (isPreview) {
|
|
1005
|
+
steps.push({
|
|
1006
|
+
name: 'Set Netlify URL',
|
|
1007
|
+
run: 'echo "DEPLOYMENT_URL=${{ steps.netlify_deploy.outputs.deploy-url }}" >> $GITHUB_ENV'
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
799
1010
|
break;
|
|
800
1011
|
}
|
|
801
1012
|
|
|
@@ -930,11 +1141,12 @@ class WorkflowGenerator {
|
|
|
930
1141
|
return lang;
|
|
931
1142
|
}
|
|
932
1143
|
|
|
933
|
-
_findScript(names) {
|
|
1144
|
+
_findScript(names, pkg = null) {
|
|
934
1145
|
const fs = require('fs');
|
|
935
1146
|
const path = require('path');
|
|
936
1147
|
try {
|
|
937
|
-
const
|
|
1148
|
+
const pkgPath = pkg ? path.join(this.projectPath, pkg.relativePath, 'package.json') : path.join(this.projectPath, 'package.json');
|
|
1149
|
+
const raw = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
938
1150
|
const scripts = raw.scripts || {};
|
|
939
1151
|
for (const n of names) {
|
|
940
1152
|
if (scripts[n]) return n;
|
|
@@ -973,6 +1185,21 @@ class WorkflowGenerator {
|
|
|
973
1185
|
});
|
|
974
1186
|
return header + raw;
|
|
975
1187
|
}
|
|
1188
|
+
|
|
1189
|
+
_getPublicEnv() {
|
|
1190
|
+
const env = {};
|
|
1191
|
+
if (this.envVars && this.envVars.public) {
|
|
1192
|
+
for (const p of this.envVars.public) {
|
|
1193
|
+
const parts = p.split('=');
|
|
1194
|
+
if (parts.length >= 2) {
|
|
1195
|
+
env[parts[0]] = parts.slice(1).join('=');
|
|
1196
|
+
} else {
|
|
1197
|
+
env[p] = `\${{ vars.${p} || secrets.${p} }}`;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return env;
|
|
1202
|
+
}
|
|
976
1203
|
}
|
|
977
1204
|
|
|
978
1205
|
module.exports = WorkflowGenerator;
|