cistack 3.1.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 +348 -92
- package/src/index.js +34 -17
- package/src/utils/helpers.js +21 -7
- package/tests/run.js +808 -0
package/src/index.js
CHANGED
|
@@ -42,17 +42,16 @@ class CIFlow {
|
|
|
42
42
|
try {
|
|
43
43
|
// ── 1. Load cistack.config.js ─────────────────────────────────────
|
|
44
44
|
const configLoader = new ConfigLoader(this.projectPath);
|
|
45
|
-
const userConfig = configLoader.load();
|
|
45
|
+
const userConfig = await configLoader.load();
|
|
46
46
|
if (Object.keys(userConfig).length > 0) {
|
|
47
47
|
spinner.info(chalk.cyan('cistack.config.js loaded'));
|
|
48
|
-
spinner.start('Scanning project...');
|
|
49
|
-
// Allow config to override outputDir
|
|
50
48
|
if (userConfig.outputDir) {
|
|
51
49
|
this.outputDir = path.join(this.projectPath, userConfig.outputDir);
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
// ── 2. Analyse the codebase ───────────────────────────────────────
|
|
54
|
+
if (!spinner.isSpinning) spinner.start('Scanning project...');
|
|
56
55
|
const analyzer = new CodebaseAnalyzer(this.projectPath, { verbose: this.verbose });
|
|
57
56
|
const codebaseInfo = await analyzer.analyse();
|
|
58
57
|
spinner.succeed(chalk.green('Project scanned'));
|
|
@@ -81,13 +80,22 @@ class CIFlow {
|
|
|
81
80
|
frameworks,
|
|
82
81
|
languages,
|
|
83
82
|
testing,
|
|
83
|
+
releaseInfo,
|
|
84
84
|
envVars,
|
|
85
85
|
monorepoPackages,
|
|
86
|
+
lockFiles: codebaseInfo.lockFiles,
|
|
87
|
+
defaultBranch: codebaseInfo.defaultBranch,
|
|
88
|
+
currentBranch: codebaseInfo.currentBranch,
|
|
86
89
|
_config: userConfig,
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
// ── 5. Print summary ───────────────────────────────────────────────
|
|
90
|
-
this._printSummary(
|
|
93
|
+
this._printSummary(
|
|
94
|
+
finalConfig,
|
|
95
|
+
finalConfig.releaseInfo || releaseInfo,
|
|
96
|
+
finalConfig.envVars || envVars,
|
|
97
|
+
finalConfig.monorepoPackages || monorepoPackages
|
|
98
|
+
);
|
|
91
99
|
|
|
92
100
|
// ── 6. Optional interactive confirmation ──────────────────────────
|
|
93
101
|
if (this.prompt) {
|
|
@@ -104,10 +112,11 @@ class CIFlow {
|
|
|
104
112
|
const dependabotGen = new DependabotGenerator(codebaseInfo);
|
|
105
113
|
const dependabotFile = dependabotGen.generate();
|
|
106
114
|
|
|
107
|
-
// ── 9. Generate release.yml (if release tooling detected)
|
|
115
|
+
// ── 9. Generate release.yml (if release tooling detected or configured) ─
|
|
108
116
|
let releaseWorkflow = null;
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
const combinedReleaseInfo = finalConfig.releaseInfo || releaseInfo;
|
|
118
|
+
if (combinedReleaseInfo) {
|
|
119
|
+
const releaseGen = new ReleaseGenerator(combinedReleaseInfo, finalConfig, this.projectPath);
|
|
111
120
|
releaseWorkflow = releaseGen.generate();
|
|
112
121
|
if (releaseWorkflow) workflows.push(releaseWorkflow);
|
|
113
122
|
}
|
|
@@ -309,7 +318,19 @@ class CIFlow {
|
|
|
309
318
|
name: HOSTING_NAME_MAP[h] || h, // always the correct PascalCase name
|
|
310
319
|
confidence: 1.0,
|
|
311
320
|
manual: true,
|
|
312
|
-
secrets
|
|
321
|
+
// Populate secrets so the generated deploy.yml header lists them correctly
|
|
322
|
+
secrets: {
|
|
323
|
+
firebase: ['FIREBASE_SERVICE_ACCOUNT'],
|
|
324
|
+
vercel: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
325
|
+
netlify: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
|
|
326
|
+
aws: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
|
|
327
|
+
gcp: ['GCP_SA_KEY'],
|
|
328
|
+
azure: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
|
|
329
|
+
heroku: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
|
|
330
|
+
render: ['RENDER_DEPLOY_HOOK_URL'],
|
|
331
|
+
railway: ['RAILWAY_TOKEN'],
|
|
332
|
+
'github-pages': [],
|
|
333
|
+
}[h] || [],
|
|
313
334
|
}));
|
|
314
335
|
}
|
|
315
336
|
|
|
@@ -376,19 +397,15 @@ class CIFlow {
|
|
|
376
397
|
ensureDir(githubDir);
|
|
377
398
|
|
|
378
399
|
if (exists && !this.force) {
|
|
400
|
+
// dependabot.yml has a fixed schema, simpler to just overwrite or keep if identical
|
|
379
401
|
const existing = fs.readFileSync(filePath, 'utf8');
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (changes.length === 0) {
|
|
402
|
+
if (existing.trim() === dependabotFile.content.trim()) {
|
|
383
403
|
console.log(chalk.dim(` ○ No changes: dependabot.yml`));
|
|
384
404
|
return;
|
|
385
405
|
}
|
|
386
|
-
|
|
387
|
-
writeFile(filePath,
|
|
388
|
-
console.log(chalk.yellow(` ↻
|
|
389
|
-
for (const c of changes) {
|
|
390
|
-
console.log(chalk.dim(` • ${c}`));
|
|
391
|
-
}
|
|
406
|
+
|
|
407
|
+
writeFile(filePath, dependabotFile.content);
|
|
408
|
+
console.log(chalk.yellow(` ↻ Updated: dependabot.yml (schema mismatch for smart-merge)`));
|
|
392
409
|
} else {
|
|
393
410
|
writeFile(filePath, dependabotFile.content);
|
|
394
411
|
console.log(chalk.green(` ✔ Written: .github/dependabot.yml`));
|
package/src/utils/helpers.js
CHANGED
|
@@ -129,16 +129,17 @@ function _mergeJob(existJob, genJob, jobId) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// Merge steps
|
|
132
|
+
// Merge steps
|
|
133
133
|
if (genJob.steps) {
|
|
134
|
-
const existStepsByName = {};
|
|
135
|
-
for (const s of (existJob.steps || [])) {
|
|
136
|
-
if (s.name) existStepsByName[s.name] = s;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
134
|
const mergedSteps = [];
|
|
140
135
|
for (const genStep of genJob.steps) {
|
|
141
|
-
|
|
136
|
+
// Find matches by name, uses, or id
|
|
137
|
+
const existStep = (existJob.steps || []).find(s =>
|
|
138
|
+
(genStep.name && s.name === genStep.name) ||
|
|
139
|
+
(genStep.id && s.id === genStep.id) ||
|
|
140
|
+
(genStep.uses && s.uses === genStep.uses)
|
|
141
|
+
);
|
|
142
|
+
|
|
142
143
|
if (!existStep) {
|
|
143
144
|
mergedSteps.push(genStep);
|
|
144
145
|
jobChanges.push(` job "${jobId}" → added step "${genStep.name}"`);
|
|
@@ -153,6 +154,19 @@ function _mergeJob(existJob, genJob, jobId) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
// Append any existing steps that were NOT matched by a generated step
|
|
158
|
+
// This ensures user customizations are preserved.
|
|
159
|
+
for (const existStep of (existJob.steps || [])) {
|
|
160
|
+
const isMatched = genJob.steps.some((genStep) =>
|
|
161
|
+
(genStep.name && existStep.name === genStep.name) ||
|
|
162
|
+
(genStep.id && existStep.id === genStep.id) ||
|
|
163
|
+
(genStep.uses && existStep.uses === genStep.uses)
|
|
164
|
+
);
|
|
165
|
+
if (!isMatched) {
|
|
166
|
+
mergedSteps.push(existStep);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
merged.steps = mergedSteps;
|
|
157
171
|
}
|
|
158
172
|
|