cistack 1.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 +173 -0
- package/bin/ciflow.js +35 -0
- package/package.json +36 -0
- package/src/analyzers/codebase.js +205 -0
- package/src/detectors/framework.js +137 -0
- package/src/detectors/hosting.js +256 -0
- package/src/detectors/language.js +116 -0
- package/src/detectors/testing.js +137 -0
- package/src/generators/workflow.js +670 -0
- package/src/index.js +166 -0
- package/src/utils/helpers.js +31 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Takes all detected signals and produces one or more complete GitHub Actions workflow YAML files.
|
|
7
|
+
*/
|
|
8
|
+
class WorkflowGenerator {
|
|
9
|
+
constructor(config, projectPath) {
|
|
10
|
+
this.hosting = config.hosting || [];
|
|
11
|
+
this.frameworks = config.frameworks || [];
|
|
12
|
+
this.languages = config.languages || [];
|
|
13
|
+
this.testing = config.testing || [];
|
|
14
|
+
this.projectPath = projectPath;
|
|
15
|
+
|
|
16
|
+
// Convenient accessors
|
|
17
|
+
this.primaryLang = this.languages[0] || { name: 'JavaScript', packageManager: 'npm', nodeVersion: '20' };
|
|
18
|
+
this.unitTests = this.testing.filter((t) => t.type === 'unit' && t.confidence > 0.3);
|
|
19
|
+
this.e2eTests = this.testing.filter((t) => t.type === 'e2e' && t.confidence > 0.3);
|
|
20
|
+
this.hasDocker = this.hosting.some((h) => h.name === 'Docker');
|
|
21
|
+
this.primaryHosting = this.hosting.filter((h) => h.name !== 'Docker')[0] || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
generate() {
|
|
25
|
+
const workflows = [];
|
|
26
|
+
|
|
27
|
+
// ── 1. Main CI workflow (lint + test + build on every push / PR) ──────
|
|
28
|
+
workflows.push({
|
|
29
|
+
filename: 'ci.yml',
|
|
30
|
+
content: this._buildCIWorkflow(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ── 2. Deploy / CD workflow (per hosting platform) ─────────────────────
|
|
34
|
+
if (this.primaryHosting) {
|
|
35
|
+
workflows.push({
|
|
36
|
+
filename: 'deploy.yml',
|
|
37
|
+
content: this._buildDeployWorkflow(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 3. Docker image build+push (if Docker detected) ──────────────────
|
|
42
|
+
if (this.hasDocker) {
|
|
43
|
+
workflows.push({
|
|
44
|
+
filename: 'docker.yml',
|
|
45
|
+
content: this._buildDockerWorkflow(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 4. Dependency update / security audit ────────────────────────────
|
|
50
|
+
workflows.push({
|
|
51
|
+
filename: 'security.yml',
|
|
52
|
+
content: this._buildSecurityWorkflow(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return workflows;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
59
|
+
// CI Workflow
|
|
60
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
_buildCIWorkflow() {
|
|
63
|
+
const lang = this.primaryLang;
|
|
64
|
+
const jobs = {};
|
|
65
|
+
|
|
66
|
+
// ── lint job ──────────────────────────────────────────────────────────
|
|
67
|
+
const lintSteps = [
|
|
68
|
+
this._stepCheckout(),
|
|
69
|
+
...this._setupSteps(lang),
|
|
70
|
+
this._stepInstallDeps(lang),
|
|
71
|
+
this._stepLint(lang),
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
|
|
74
|
+
jobs.lint = {
|
|
75
|
+
name: '🔍 Lint & Format',
|
|
76
|
+
'runs-on': 'ubuntu-latest',
|
|
77
|
+
steps: lintSteps,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── test job ──────────────────────────────────────────────────────────
|
|
81
|
+
if (this.unitTests.length > 0) {
|
|
82
|
+
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
|
+
jobs.test = {
|
|
92
|
+
name: '🧪 Unit Tests',
|
|
93
|
+
'runs-on': 'ubuntu-latest',
|
|
94
|
+
...(testMatrix ? { strategy: testMatrix } : {}),
|
|
95
|
+
steps: testSteps,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── build job ─────────────────────────────────────────────────────────
|
|
100
|
+
const buildSteps = this._buildSteps(lang);
|
|
101
|
+
if (buildSteps.length > 0) {
|
|
102
|
+
jobs.build = {
|
|
103
|
+
name: '🏗️ Build',
|
|
104
|
+
'runs-on': 'ubuntu-latest',
|
|
105
|
+
needs: [
|
|
106
|
+
'lint',
|
|
107
|
+
...(jobs.test ? ['test'] : []),
|
|
108
|
+
],
|
|
109
|
+
steps: [
|
|
110
|
+
this._stepCheckout(),
|
|
111
|
+
...this._setupSteps(lang),
|
|
112
|
+
this._stepInstallDeps(lang),
|
|
113
|
+
...buildSteps,
|
|
114
|
+
this._stepUploadArtifact(),
|
|
115
|
+
].filter(Boolean),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── e2e job ───────────────────────────────────────────────────────────
|
|
120
|
+
if (this.e2eTests.length > 0) {
|
|
121
|
+
const e2eTest = this.e2eTests[0];
|
|
122
|
+
jobs['e2e'] = {
|
|
123
|
+
name: '🎭 E2E Tests',
|
|
124
|
+
'runs-on': 'ubuntu-latest',
|
|
125
|
+
needs: ['build'],
|
|
126
|
+
steps: [
|
|
127
|
+
this._stepCheckout(),
|
|
128
|
+
...this._setupSteps(lang),
|
|
129
|
+
this._stepInstallDeps(lang),
|
|
130
|
+
...(e2eTest.name === 'Playwright' ? [{ name: 'Install Playwright browsers', run: 'npx playwright install --with-deps' }] : []),
|
|
131
|
+
{ name: `Run ${e2eTest.name}`, run: e2eTest.command },
|
|
132
|
+
{
|
|
133
|
+
name: 'Upload E2E report',
|
|
134
|
+
if: 'always()',
|
|
135
|
+
uses: 'actions/upload-artifact@v4',
|
|
136
|
+
with: {
|
|
137
|
+
name: 'e2e-report',
|
|
138
|
+
path: e2eTest.name === 'Playwright' ? 'playwright-report/' : 'cypress/screenshots/',
|
|
139
|
+
'retention-days': 7,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
].filter(Boolean),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const workflow = {
|
|
147
|
+
name: 'CI',
|
|
148
|
+
on: {
|
|
149
|
+
push: { branches: ['main', 'master', 'develop'] },
|
|
150
|
+
pull_request: { branches: ['main', 'master', 'develop'] },
|
|
151
|
+
},
|
|
152
|
+
'concurrency': {
|
|
153
|
+
group: '${{ github.workflow }}-${{ github.ref }}',
|
|
154
|
+
'cancel-in-progress': true,
|
|
155
|
+
},
|
|
156
|
+
jobs,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return this._toYaml(workflow, `# Generated by cistack — https://github.com/cistack\n# CI Pipeline: lint → test → build${this.e2eTests.length > 0 ? ' → e2e' : ''}\n\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
163
|
+
// Deploy Workflow
|
|
164
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
_buildDeployWorkflow() {
|
|
167
|
+
const h = this.primaryHosting;
|
|
168
|
+
const lang = this.primaryLang;
|
|
169
|
+
|
|
170
|
+
const preDeploySteps = [
|
|
171
|
+
this._stepCheckout(),
|
|
172
|
+
...this._setupSteps(lang),
|
|
173
|
+
this._stepInstallDeps(lang),
|
|
174
|
+
].filter(Boolean);
|
|
175
|
+
|
|
176
|
+
const deploySteps = this._hostingDeploySteps(h, lang);
|
|
177
|
+
|
|
178
|
+
const jobs = {
|
|
179
|
+
deploy: {
|
|
180
|
+
name: `🚀 Deploy → ${h.name}`,
|
|
181
|
+
'runs-on': 'ubuntu-latest',
|
|
182
|
+
environment: 'production',
|
|
183
|
+
steps: [...preDeploySteps, ...deploySteps].filter(Boolean),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const secretsDoc = h.secrets.length > 0
|
|
188
|
+
? `# Required secrets: ${h.secrets.join(', ')}\n# Add these at: Settings → Secrets and Variables → Actions\n\n`
|
|
189
|
+
: '';
|
|
190
|
+
|
|
191
|
+
const workflow = {
|
|
192
|
+
name: `Deploy to ${h.name}`,
|
|
193
|
+
on: {
|
|
194
|
+
push: { branches: ['main', 'master'] },
|
|
195
|
+
workflow_dispatch: {},
|
|
196
|
+
},
|
|
197
|
+
jobs,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return this._toYaml(workflow, `# Generated by cistack\n# Deploy Pipeline → ${h.name}\n${secretsDoc}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// Docker Workflow
|
|
205
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
|
|
207
|
+
_buildDockerWorkflow() {
|
|
208
|
+
const workflow = {
|
|
209
|
+
name: 'Docker Build & Push',
|
|
210
|
+
on: {
|
|
211
|
+
push: {
|
|
212
|
+
branches: ['main', 'master'],
|
|
213
|
+
tags: ['v*.*.*'],
|
|
214
|
+
},
|
|
215
|
+
pull_request: { branches: ['main', 'master'] },
|
|
216
|
+
},
|
|
217
|
+
env: {
|
|
218
|
+
REGISTRY: 'ghcr.io',
|
|
219
|
+
IMAGE_NAME: '${{ github.repository }}',
|
|
220
|
+
},
|
|
221
|
+
jobs: {
|
|
222
|
+
build: {
|
|
223
|
+
name: '🐳 Build & Push Docker Image',
|
|
224
|
+
'runs-on': 'ubuntu-latest',
|
|
225
|
+
permissions: {
|
|
226
|
+
contents: 'read',
|
|
227
|
+
packages: 'write',
|
|
228
|
+
},
|
|
229
|
+
steps: [
|
|
230
|
+
this._stepCheckout(),
|
|
231
|
+
{
|
|
232
|
+
name: 'Set up Docker Buildx',
|
|
233
|
+
uses: 'docker/setup-buildx-action@v3',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'Log in to Container Registry',
|
|
237
|
+
if: "github.event_name != 'pull_request'",
|
|
238
|
+
uses: 'docker/login-action@v3',
|
|
239
|
+
with: {
|
|
240
|
+
registry: '${{ env.REGISTRY }}',
|
|
241
|
+
username: '${{ github.actor }}',
|
|
242
|
+
password: '${{ secrets.GITHUB_TOKEN }}',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'Extract metadata',
|
|
247
|
+
id: 'meta',
|
|
248
|
+
uses: 'docker/metadata-action@v5',
|
|
249
|
+
with: {
|
|
250
|
+
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}',
|
|
251
|
+
tags: [
|
|
252
|
+
'type=ref,event=branch',
|
|
253
|
+
'type=ref,event=pr',
|
|
254
|
+
'type=semver,pattern={{version}}',
|
|
255
|
+
'type=semver,pattern={{major}}.{{minor}}',
|
|
256
|
+
'type=sha',
|
|
257
|
+
].join('\n'),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'Build and push',
|
|
262
|
+
uses: 'docker/build-push-action@v5',
|
|
263
|
+
with: {
|
|
264
|
+
context: '.',
|
|
265
|
+
push: "${{ github.event_name != 'pull_request' }}",
|
|
266
|
+
tags: '${{ steps.meta.outputs.tags }}',
|
|
267
|
+
labels: '${{ steps.meta.outputs.labels }}',
|
|
268
|
+
cache_from: 'type=gha',
|
|
269
|
+
cache_to: 'type=gha,mode=max',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return this._toYaml(workflow, '# Generated by cistack\n# Docker image build and push to GHCR\n\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
281
|
+
// Security Workflow
|
|
282
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
283
|
+
|
|
284
|
+
_buildSecurityWorkflow() {
|
|
285
|
+
const lang = this.primaryLang;
|
|
286
|
+
const steps = [this._stepCheckout()];
|
|
287
|
+
|
|
288
|
+
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
289
|
+
steps.push(
|
|
290
|
+
...this._setupSteps(lang),
|
|
291
|
+
this._stepInstallDeps(lang),
|
|
292
|
+
{ name: 'Audit dependencies', run: lang.packageManager === 'npm' ? 'npm audit --audit-level=high' : lang.packageManager === 'yarn' ? 'yarn audit --level high' : 'pnpm audit --audit-level high' },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (lang.name === 'Python') {
|
|
297
|
+
steps.push(
|
|
298
|
+
{ name: 'Set up Python', uses: 'actions/setup-python@v5', with: { 'python-version': '3.x' } },
|
|
299
|
+
{ name: 'Install safety', run: 'pip install safety' },
|
|
300
|
+
{ name: 'Run safety check', run: 'safety check' },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// CodeQL analysis
|
|
305
|
+
steps.push(
|
|
306
|
+
{
|
|
307
|
+
name: 'Initialize CodeQL',
|
|
308
|
+
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',
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const workflow = {
|
|
320
|
+
name: 'Security Audit',
|
|
321
|
+
on: {
|
|
322
|
+
push: { branches: ['main', 'master'] },
|
|
323
|
+
pull_request: { branches: ['main', 'master'] },
|
|
324
|
+
schedule: [{ cron: '0 6 * * 1' }], // Every Monday 6am
|
|
325
|
+
},
|
|
326
|
+
jobs: {
|
|
327
|
+
security: {
|
|
328
|
+
name: '🔒 Security Audit',
|
|
329
|
+
'runs-on': 'ubuntu-latest',
|
|
330
|
+
permissions: {
|
|
331
|
+
actions: 'read',
|
|
332
|
+
contents: 'read',
|
|
333
|
+
'security-events': 'write',
|
|
334
|
+
},
|
|
335
|
+
steps: steps.filter(Boolean),
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return this._toYaml(workflow, '# Generated by cistack\n# Security: dependency audit + CodeQL analysis (runs weekly)\n\n');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
344
|
+
// Reusable step builders
|
|
345
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
346
|
+
|
|
347
|
+
_stepCheckout() {
|
|
348
|
+
return { name: 'Checkout code', uses: 'actions/checkout@v4', with: { 'fetch-depth': 0 } };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_setupSteps(lang) {
|
|
352
|
+
const steps = [];
|
|
353
|
+
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
354
|
+
steps.push({
|
|
355
|
+
name: 'Set up Node.js',
|
|
356
|
+
uses: 'actions/setup-node@v4',
|
|
357
|
+
with: {
|
|
358
|
+
'node-version': lang.nodeVersion || '20',
|
|
359
|
+
cache: lang.packageManager === 'yarn' ? 'yarn' : lang.packageManager === 'pnpm' ? 'pnpm' : 'npm',
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
if (lang.packageManager === 'pnpm') {
|
|
363
|
+
steps.unshift({ name: 'Install pnpm', uses: 'pnpm/action-setup@v3', with: { version: 'latest' } });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (lang.name === 'Python') {
|
|
367
|
+
steps.push({ name: 'Set up Python', uses: 'actions/setup-python@v5', with: { 'python-version': '3.x' } });
|
|
368
|
+
}
|
|
369
|
+
if (lang.name === 'Go') {
|
|
370
|
+
steps.push({ name: 'Set up Go', uses: 'actions/setup-go@v5', with: { 'go-version': 'stable' } });
|
|
371
|
+
}
|
|
372
|
+
if (lang.name === 'Java' || lang.name === 'Kotlin') {
|
|
373
|
+
steps.push({ name: 'Set up JDK', uses: 'actions/setup-java@v4', with: { 'java-version': '21', distribution: 'temurin' } });
|
|
374
|
+
}
|
|
375
|
+
if (lang.name === 'Ruby') {
|
|
376
|
+
steps.push({ name: 'Set up Ruby', uses: 'ruby/setup-ruby@v1', with: { 'bundler-cache': true } });
|
|
377
|
+
}
|
|
378
|
+
if (lang.name === 'Rust') {
|
|
379
|
+
steps.push({ name: 'Set up Rust', uses: 'dtolnay/rust-toolchain@stable' });
|
|
380
|
+
}
|
|
381
|
+
return steps;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_stepInstallDeps(lang) {
|
|
385
|
+
const pm = lang.packageManager;
|
|
386
|
+
if (pm === 'npm') return { name: 'Install dependencies', run: 'npm ci' };
|
|
387
|
+
if (pm === 'yarn') return { name: 'Install dependencies', run: 'yarn install --frozen-lockfile' };
|
|
388
|
+
if (pm === 'pnpm') return { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' };
|
|
389
|
+
if (pm === 'bun') return { name: 'Install dependencies', run: 'bun install' };
|
|
390
|
+
if (pm === 'pip') return { name: 'Install dependencies', run: 'pip install -r requirements.txt' };
|
|
391
|
+
if (pm === 'poetry') return { name: 'Install dependencies', run: 'pip install poetry && poetry install' };
|
|
392
|
+
if (pm === 'pipenv') return { name: 'Install dependencies', run: 'pip install pipenv && pipenv install --dev' };
|
|
393
|
+
if (pm === 'bundler') return { name: 'Install dependencies', run: 'bundle install' };
|
|
394
|
+
if (pm === 'go mod') return { name: 'Download modules', run: 'go mod download' };
|
|
395
|
+
if (pm === 'cargo') return null; // Cargo handles deps on build/test
|
|
396
|
+
if (pm === 'maven') return { name: 'Cache Maven', uses: 'actions/cache@v4', with: { path: '~/.m2', key: "${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}" } };
|
|
397
|
+
if (pm === 'composer') return { name: 'Install dependencies', run: 'composer install --no-interaction --prefer-dist --optimize-autoloader' };
|
|
398
|
+
return { name: 'Install dependencies', run: 'npm ci' };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
_stepLint(lang) {
|
|
402
|
+
const pm = lang.packageManager || 'npm';
|
|
403
|
+
const scripts = (this.languages[0] || {}).scripts || {};
|
|
404
|
+
|
|
405
|
+
if (['JavaScript', 'TypeScript'].includes(lang.name)) {
|
|
406
|
+
// Pick the right lint command
|
|
407
|
+
const lintScript = this._findScript(['lint', 'lint:ci', 'eslint']);
|
|
408
|
+
const typeCheck = this._findScript(['type-check', 'typecheck', 'tsc']);
|
|
409
|
+
const format = this._findScript(['format:check', 'prettier:check', 'fmt:check']);
|
|
410
|
+
|
|
411
|
+
const cmds = [];
|
|
412
|
+
if (lintScript) cmds.push(`${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run'} ${lintScript}`);
|
|
413
|
+
if (typeCheck) cmds.push(`${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run'} ${typeCheck}`);
|
|
414
|
+
if (format) cmds.push(`${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run'} ${format}`);
|
|
415
|
+
if (cmds.length === 0) cmds.push('npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0');
|
|
416
|
+
|
|
417
|
+
return { name: 'Lint', run: cmds.join('\n') };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (lang.name === 'Python') return { name: 'Lint', run: 'pip install flake8 && flake8 .' };
|
|
421
|
+
if (lang.name === 'Go') return { name: 'Lint', run: 'gofmt -d . && go vet ./...' };
|
|
422
|
+
if (lang.name === 'Rust') return { name: 'Lint', run: 'cargo clippy -- -D warnings && cargo fmt --check' };
|
|
423
|
+
if (lang.name === 'Ruby') return { name: 'Lint', run: 'gem install rubocop && rubocop' };
|
|
424
|
+
if (lang.name === 'PHP') return { name: 'Lint', run: 'vendor/bin/phpcs' };
|
|
425
|
+
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_unitTestSteps(lang) {
|
|
430
|
+
return this.unitTests.map((t) => ({
|
|
431
|
+
name: `Run ${t.name}`,
|
|
432
|
+
run: t.command,
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_buildSteps(lang) {
|
|
437
|
+
const pm = lang.packageManager || 'npm';
|
|
438
|
+
const buildScript = this._findScript(['build', 'build:prod']);
|
|
439
|
+
|
|
440
|
+
if (!buildScript && !['Go', 'Rust', 'Java', 'Kotlin'].includes(lang.name)) return [];
|
|
441
|
+
|
|
442
|
+
const steps = [];
|
|
443
|
+
|
|
444
|
+
if (['JavaScript', 'TypeScript'].includes(lang.name) && buildScript) {
|
|
445
|
+
steps.push({
|
|
446
|
+
name: 'Build',
|
|
447
|
+
run: `${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm'} run ${buildScript}`,
|
|
448
|
+
env: { NODE_ENV: 'production' },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (lang.name === 'Go') steps.push({ name: 'Build', run: 'go build -v ./...' });
|
|
452
|
+
if (lang.name === 'Rust') steps.push({ name: 'Build', run: 'cargo build --release' });
|
|
453
|
+
if (lang.name === 'Java') steps.push({ name: 'Build', run: 'mvn -B package --no-transfer-progress -DskipTests' });
|
|
454
|
+
if (lang.name === 'Python') { /* Python typically doesn't have a build step */ }
|
|
455
|
+
|
|
456
|
+
return steps;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_stepUploadArtifact() {
|
|
460
|
+
const buildDir = (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist';
|
|
461
|
+
return {
|
|
462
|
+
name: 'Upload build artifact',
|
|
463
|
+
uses: 'actions/upload-artifact@v4',
|
|
464
|
+
with: {
|
|
465
|
+
name: 'build-output',
|
|
466
|
+
path: buildDir,
|
|
467
|
+
'retention-days': 1,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
_stepUploadCoverage() {
|
|
473
|
+
const hasCoverage = this.unitTests.some((t) =>
|
|
474
|
+
t.command.includes('coverage') || t.command.includes('--cov')
|
|
475
|
+
);
|
|
476
|
+
if (!hasCoverage) return null;
|
|
477
|
+
return {
|
|
478
|
+
name: 'Upload coverage report',
|
|
479
|
+
uses: 'codecov/codecov-action@v4',
|
|
480
|
+
with: { token: '${{ secrets.CODECOV_TOKEN }}', fail_ci_if_error: false },
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_testMatrix(lang) {
|
|
485
|
+
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
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (lang.name === 'Python') {
|
|
494
|
+
return { matrix: { 'python-version': ['3.10', '3.11', '3.12'] }, 'fail-fast': false };
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
500
|
+
// Hosting-specific deploy steps
|
|
501
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
502
|
+
|
|
503
|
+
_hostingDeploySteps(h, lang) {
|
|
504
|
+
const steps = [];
|
|
505
|
+
const buildScript = this._findScript(['build', 'build:prod']);
|
|
506
|
+
const pm = lang.packageManager || 'npm';
|
|
507
|
+
const runCmd = (s) => `${pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm'} run ${s}`;
|
|
508
|
+
|
|
509
|
+
switch (h.name) {
|
|
510
|
+
case 'Firebase': {
|
|
511
|
+
if (buildScript) {
|
|
512
|
+
steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
513
|
+
}
|
|
514
|
+
steps.push({
|
|
515
|
+
name: 'Deploy to Firebase',
|
|
516
|
+
uses: 'FirebaseExtended/action-hosting-deploy@v0',
|
|
517
|
+
with: {
|
|
518
|
+
repoToken: '${{ secrets.GITHUB_TOKEN }}',
|
|
519
|
+
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}',
|
|
520
|
+
channelId: 'live',
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
case 'Vercel': {
|
|
527
|
+
steps.push(
|
|
528
|
+
{ name: 'Install Vercel CLI', run: 'npm install -g vercel' },
|
|
529
|
+
{ name: 'Pull Vercel environment', run: 'vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}' },
|
|
530
|
+
{ name: 'Build project', run: 'vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}' },
|
|
531
|
+
{ name: 'Deploy to Vercel', run: 'vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}' },
|
|
532
|
+
);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
case 'Netlify': {
|
|
537
|
+
if (buildScript) {
|
|
538
|
+
steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
539
|
+
}
|
|
540
|
+
steps.push({
|
|
541
|
+
name: 'Deploy to Netlify',
|
|
542
|
+
uses: 'nwtgck/actions-netlify@v3.0',
|
|
543
|
+
with: {
|
|
544
|
+
'publish-dir': h.publishDir || 'dist',
|
|
545
|
+
'production-branch': 'main',
|
|
546
|
+
'github-token': '${{ secrets.GITHUB_TOKEN }}',
|
|
547
|
+
'deploy-message': "Deploy from GitHub Actions – ${{ github.sha }}",
|
|
548
|
+
'enable-pull-request-comment': true,
|
|
549
|
+
'enable-commit-comment': true,
|
|
550
|
+
},
|
|
551
|
+
env: {
|
|
552
|
+
NETLIFY_AUTH_TOKEN: '${{ secrets.NETLIFY_AUTH_TOKEN }}',
|
|
553
|
+
NETLIFY_SITE_ID: '${{ secrets.NETLIFY_SITE_ID }}',
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
case 'GitHub Pages': {
|
|
560
|
+
if (buildScript) {
|
|
561
|
+
steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
562
|
+
}
|
|
563
|
+
steps.push(
|
|
564
|
+
{ name: 'Setup Pages', uses: 'actions/configure-pages@v4' },
|
|
565
|
+
{ name: 'Upload Pages artifact', uses: 'actions/upload-pages-artifact@v3', with: { path: (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist' } },
|
|
566
|
+
{ name: 'Deploy to GitHub Pages', id: 'deployment', uses: 'actions/deploy-pages@v4' },
|
|
567
|
+
);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
case 'AWS': {
|
|
572
|
+
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript), env: { NODE_ENV: 'production' } });
|
|
573
|
+
const awsBuildDir = (this.frameworks[0] && this.frameworks[0].buildDir) || 'dist';
|
|
574
|
+
steps.push(
|
|
575
|
+
{ name: 'Configure AWS credentials', uses: 'aws-actions/configure-aws-credentials@v4', with: { 'aws-access-key-id': '${{ secrets.AWS_ACCESS_KEY_ID }}', 'aws-secret-access-key': '${{ secrets.AWS_SECRET_ACCESS_KEY }}', 'aws-region': '${{ secrets.AWS_REGION }}' } },
|
|
576
|
+
{ name: 'Sync to S3', run: 'aws s3 sync ./' + awsBuildDir + ' s3://${{ secrets.AWS_S3_BUCKET }} --delete' },
|
|
577
|
+
{ name: 'Invalidate CloudFront', run: 'aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"' },
|
|
578
|
+
);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
case 'GCP App Engine': {
|
|
583
|
+
steps.push(
|
|
584
|
+
{ name: 'Auth to Google Cloud', uses: 'google-github-actions/auth@v2', with: { credentials_json: '${{ secrets.GCP_SA_KEY }}' } },
|
|
585
|
+
{ name: 'Set up Cloud SDK', uses: 'google-github-actions/setup-gcloud@v2' },
|
|
586
|
+
{ name: 'Deploy to App Engine', run: 'gcloud app deploy --quiet' },
|
|
587
|
+
);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
case 'Heroku': {
|
|
592
|
+
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript) });
|
|
593
|
+
steps.push({ name: 'Deploy to Heroku', uses: 'akhileshns/heroku-deploy@v3.13.15', with: { heroku_api_key: '${{ secrets.HEROKU_API_KEY }}', heroku_app_name: '${{ secrets.HEROKU_APP_NAME }}', heroku_email: '${{ secrets.HEROKU_EMAIL }}' } });
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
case 'Render': {
|
|
598
|
+
steps.push({ name: 'Trigger Render deploy', run: 'curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}' });
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'Railway': {
|
|
603
|
+
steps.push(
|
|
604
|
+
{ name: 'Install Railway CLI', run: 'npm install -g @railway/cli' },
|
|
605
|
+
{ name: 'Deploy to Railway', run: 'railway up', env: { RAILWAY_TOKEN: '${{ secrets.RAILWAY_TOKEN }}' } },
|
|
606
|
+
);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'Azure': {
|
|
611
|
+
if (buildScript) steps.push({ name: 'Build', run: runCmd(buildScript) });
|
|
612
|
+
steps.push({ name: 'Deploy to Azure Web App', uses: 'azure/webapps-deploy@v3', with: { 'app-name': '${{ secrets.AZURE_APP_NAME }}', 'publish-profile': '${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}', package: (this.frameworks[0] && this.frameworks[0].buildDir) || '.' } });
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
default:
|
|
617
|
+
steps.push({ name: 'Deploy', run: h.deployCommand || 'echo "No deploy command configured"' });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return steps;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
624
|
+
// Utility helpers
|
|
625
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
626
|
+
|
|
627
|
+
_findScript(names) {
|
|
628
|
+
const pkg = this.languages[0];
|
|
629
|
+
// Access from the raw packageJson in projectPath
|
|
630
|
+
const fs = require('fs');
|
|
631
|
+
const path = require('path');
|
|
632
|
+
try {
|
|
633
|
+
const raw = JSON.parse(fs.readFileSync(path.join(this.projectPath, 'package.json'), 'utf8'));
|
|
634
|
+
const scripts = raw.scripts || {};
|
|
635
|
+
for (const n of names) {
|
|
636
|
+
if (scripts[n]) return n;
|
|
637
|
+
}
|
|
638
|
+
} catch (_) {}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
_codeQLLanguage(langName) {
|
|
643
|
+
const map = {
|
|
644
|
+
'JavaScript': 'javascript',
|
|
645
|
+
'TypeScript': 'javascript',
|
|
646
|
+
'Python': 'python',
|
|
647
|
+
'Ruby': 'ruby',
|
|
648
|
+
'Go': 'go',
|
|
649
|
+
'Java': 'java',
|
|
650
|
+
'Kotlin': 'java',
|
|
651
|
+
'C#': 'csharp',
|
|
652
|
+
'C++': 'cpp',
|
|
653
|
+
'C': 'cpp',
|
|
654
|
+
};
|
|
655
|
+
return map[langName] || 'javascript';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_toYaml(obj, header = '') {
|
|
659
|
+
const raw = yaml.dump(obj, {
|
|
660
|
+
indent: 2,
|
|
661
|
+
lineWidth: 120,
|
|
662
|
+
quotingType: "'",
|
|
663
|
+
forceQuotes: false,
|
|
664
|
+
noRefs: true,
|
|
665
|
+
});
|
|
666
|
+
return header + raw;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
module.exports = WorkflowGenerator;
|