cistack 1.0.0 → 2.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/bin/ciflow.js +66 -7
- package/package.json +10 -3
- package/src/analyzers/monorepo.js +124 -0
- package/src/config/loader.js +123 -0
- package/src/detectors/env.js +69 -0
- 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 +431 -115
- package/src/index.js +162 -53
- package/src/utils/helpers.js +146 -9
package/bin/ciflow.js
CHANGED
|
@@ -9,7 +9,7 @@ const CIFlow = require('../src/index');
|
|
|
9
9
|
program
|
|
10
10
|
.name('cistack')
|
|
11
11
|
.description('Generate GitHub Actions CI/CD pipelines by analysing your codebase')
|
|
12
|
-
.version('
|
|
12
|
+
.version('2.0.0');
|
|
13
13
|
|
|
14
14
|
program
|
|
15
15
|
.command('generate', { isDefault: true })
|
|
@@ -17,19 +17,78 @@ program
|
|
|
17
17
|
.option('-p, --path <dir>', 'Path to the project root', process.cwd())
|
|
18
18
|
.option('-o, --output <dir>', 'Output directory for workflow files', '.github/workflows')
|
|
19
19
|
.option('--dry-run', 'Print the generated YAML without writing files')
|
|
20
|
-
.option('--force', 'Overwrite existing workflow files without
|
|
20
|
+
.option('--force', 'Overwrite existing workflow files without smart-merge')
|
|
21
21
|
.option('--no-prompt', 'Skip interactive prompts and use detected settings')
|
|
22
22
|
.option('--verbose', 'Show detailed analysis output')
|
|
23
23
|
.action(async (options) => {
|
|
24
24
|
const ciflow = new CIFlow({
|
|
25
25
|
projectPath: path.resolve(options.path),
|
|
26
|
-
outputDir:
|
|
27
|
-
dryRun:
|
|
28
|
-
force:
|
|
29
|
-
prompt:
|
|
30
|
-
verbose:
|
|
26
|
+
outputDir: options.output,
|
|
27
|
+
dryRun: options.dryRun,
|
|
28
|
+
force: options.force,
|
|
29
|
+
prompt: options.prompt,
|
|
30
|
+
verbose: options.verbose,
|
|
31
31
|
});
|
|
32
32
|
await ciflow.run();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
program
|
|
36
|
+
.command('init')
|
|
37
|
+
.description('Create a starter cistack.config.js in the current directory')
|
|
38
|
+
.option('-p, --path <dir>', 'Path to the project root', process.cwd())
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const resolvedPath = path.resolve(options.path);
|
|
42
|
+
const configPath = path.join(resolvedPath, 'cistack.config.js');
|
|
43
|
+
|
|
44
|
+
// Ensure the target directory exists
|
|
45
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
46
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(configPath)) {
|
|
50
|
+
console.error('cistack.config.js already exists. Delete it first or edit it manually.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const template = `// cistack.config.js
|
|
55
|
+
// Override auto-detected settings for this project.
|
|
56
|
+
// All fields are optional — omit what you don't need.
|
|
57
|
+
|
|
58
|
+
/** @type {import('cistack').Config} */
|
|
59
|
+
module.exports = {
|
|
60
|
+
// nodeVersion: '20', // Override detected Node.js version
|
|
61
|
+
// packageManager: 'pnpm', // 'npm' | 'yarn' | 'pnpm' | 'bun'
|
|
62
|
+
// hosting: ['Firebase'], // Force a specific hosting provider
|
|
63
|
+
// branches: ['main', 'staging'], // CI branches (default: main, master, develop)
|
|
64
|
+
// outputDir: '.github/workflows', // Where to write workflow files
|
|
65
|
+
|
|
66
|
+
// cache: {
|
|
67
|
+
// npm: true, // enabled by default
|
|
68
|
+
// pip: true,
|
|
69
|
+
// cargo: true,
|
|
70
|
+
// maven: true,
|
|
71
|
+
// gradle: true,
|
|
72
|
+
// go: true,
|
|
73
|
+
// composer: true,
|
|
74
|
+
// },
|
|
75
|
+
|
|
76
|
+
// monorepo: {
|
|
77
|
+
// perPackage: true, // Generate one ci-<name>.yml per workspace
|
|
78
|
+
// },
|
|
79
|
+
|
|
80
|
+
// release: {
|
|
81
|
+
// tool: 'semantic-release', // override release tool detection
|
|
82
|
+
// },
|
|
83
|
+
|
|
84
|
+
// secrets: ['MY_EXTRA_SECRET'], // Document additional secrets in workflow comments
|
|
85
|
+
};
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(configPath, template, 'utf8');
|
|
89
|
+
const chalk = require('chalk');
|
|
90
|
+
console.log(chalk.green(`✔ Created cistack.config.js at ${configPath}`));
|
|
91
|
+
console.log(chalk.dim(' Edit the file to override detected settings.'));
|
|
92
|
+
});
|
|
93
|
+
|
|
35
94
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cistack",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Automatically generate GitHub Actions CI/CD pipelines by analysing your codebase",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/ciflow.js",
|
|
11
|
-
"test": "node bin/ciflow.js --dry-run"
|
|
11
|
+
"test": "node bin/ciflow.js --dry-run --no-prompt"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"github-actions",
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
"devops",
|
|
19
19
|
"firebase",
|
|
20
20
|
"vercel",
|
|
21
|
-
"netlify"
|
|
21
|
+
"netlify",
|
|
22
|
+
"monorepo",
|
|
23
|
+
"dependabot",
|
|
24
|
+
"semantic-release",
|
|
25
|
+
"changesets",
|
|
26
|
+
"caching",
|
|
27
|
+
"turborepo",
|
|
28
|
+
"nx"
|
|
22
29
|
],
|
|
23
30
|
"author": "",
|
|
24
31
|
"license": "MIT",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { globSync } = require('glob');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves workspace packages from monorepo config files.
|
|
9
|
+
*
|
|
10
|
+
* Supported:
|
|
11
|
+
* - package.json#workspaces (npm/yarn workspaces array or { packages: [] })
|
|
12
|
+
* - pnpm-workspace.yaml
|
|
13
|
+
* - turbo.json (uses package.json workspaces in conjunction)
|
|
14
|
+
* - nx.json (discovers apps/* and libs/*)
|
|
15
|
+
* - lerna.json (uses lerna packages array)
|
|
16
|
+
*
|
|
17
|
+
* Returns: Array<{ name: string, relativePath: string, absolutePath: string, packageJson: object|null }>
|
|
18
|
+
*/
|
|
19
|
+
class MonorepoAnalyzer {
|
|
20
|
+
constructor(projectPath, codebaseInfo) {
|
|
21
|
+
this.root = projectPath;
|
|
22
|
+
this.info = codebaseInfo;
|
|
23
|
+
this.pkg = codebaseInfo.packageJson || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async analyze() {
|
|
27
|
+
if (!this.info.hasMonorepo) return [];
|
|
28
|
+
|
|
29
|
+
const workspaceGlobs = this._collectWorkspaceGlobs();
|
|
30
|
+
if (workspaceGlobs.length === 0) return [];
|
|
31
|
+
|
|
32
|
+
const packages = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
|
|
35
|
+
for (const pattern of workspaceGlobs) {
|
|
36
|
+
// Each glob glob like 'packages/*' → expand to actual dirs
|
|
37
|
+
const matches = globSync(pattern, {
|
|
38
|
+
cwd: this.root,
|
|
39
|
+
onlyDirectories: true,
|
|
40
|
+
absolute: false,
|
|
41
|
+
ignore: ['node_modules/**', '**/node_modules/**'],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for (const relDir of matches) {
|
|
45
|
+
if (seen.has(relDir)) continue;
|
|
46
|
+
seen.add(relDir);
|
|
47
|
+
|
|
48
|
+
const absPath = path.join(this.root, relDir);
|
|
49
|
+
const pkgJsonPath = path.join(absPath, 'package.json');
|
|
50
|
+
let pkgJson = null;
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
53
|
+
try {
|
|
54
|
+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const name = (pkgJson && pkgJson.name) || path.basename(relDir);
|
|
59
|
+
|
|
60
|
+
packages.push({
|
|
61
|
+
name,
|
|
62
|
+
relativePath: relDir,
|
|
63
|
+
absolutePath: absPath,
|
|
64
|
+
packageJson: pkgJson,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return packages;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_collectWorkspaceGlobs() {
|
|
73
|
+
const globs = [];
|
|
74
|
+
|
|
75
|
+
// 1. package.json workspaces
|
|
76
|
+
if (this.pkg.workspaces) {
|
|
77
|
+
const ws = this.pkg.workspaces;
|
|
78
|
+
if (Array.isArray(ws)) {
|
|
79
|
+
globs.push(...ws);
|
|
80
|
+
} else if (ws.packages) {
|
|
81
|
+
globs.push(...ws.packages);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. pnpm-workspace.yaml
|
|
86
|
+
const pnpmWsPath = path.join(this.root, 'pnpm-workspace.yaml');
|
|
87
|
+
if (fs.existsSync(pnpmWsPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const yaml = require('js-yaml');
|
|
90
|
+
const parsed = yaml.load(fs.readFileSync(pnpmWsPath, 'utf8'));
|
|
91
|
+
if (parsed && parsed.packages) {
|
|
92
|
+
globs.push(...parsed.packages);
|
|
93
|
+
}
|
|
94
|
+
} catch (_) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. lerna.json
|
|
98
|
+
const lernaPath = path.join(this.root, 'lerna.json');
|
|
99
|
+
if (fs.existsSync(lernaPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
|
|
102
|
+
if (lerna.packages) globs.push(...lerna.packages);
|
|
103
|
+
} catch (_) {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. nx.json — default convention: apps/* + libs/*
|
|
107
|
+
const nxPath = path.join(this.root, 'nx.json');
|
|
108
|
+
if (fs.existsSync(nxPath) && globs.length === 0) {
|
|
109
|
+
globs.push('apps/*', 'libs/*', 'packages/*');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 5. turbo.json — uses root package.json workspaces (already handled above)
|
|
113
|
+
// If none found yet, use convention
|
|
114
|
+
const turboPath = path.join(this.root, 'turbo.json');
|
|
115
|
+
if (fs.existsSync(turboPath) && globs.length === 0) {
|
|
116
|
+
globs.push('apps/*', 'packages/*');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Deduplicate
|
|
120
|
+
return [...new Set(globs)];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = MonorepoAnalyzer;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loads cistack.config.js (or .cjs / .mjs) from the project root.
|
|
8
|
+
* Returns an empty object if no config file is found.
|
|
9
|
+
*
|
|
10
|
+
* Supported keys:
|
|
11
|
+
* nodeVersion – override detected Node version e.g. '18'
|
|
12
|
+
* packageManager – override detected PM: 'npm'|'yarn'|'pnpm'|'bun'
|
|
13
|
+
* hosting – array of hosting names to force e.g. ['Firebase']
|
|
14
|
+
* branches – branches to run CI on e.g. ['main', 'staging']
|
|
15
|
+
* cache – { npm: bool, cargo: bool, pip: bool, ... } enable/disable caches
|
|
16
|
+
* monorepo – { perPackage: bool } generate one file per workspace
|
|
17
|
+
* release – { tool: 'semantic-release'|'changesets'|'standard-version'|'release-it' }
|
|
18
|
+
* secrets – extra secret names to document in workflow comments
|
|
19
|
+
* outputDir – override default '.github/workflows'
|
|
20
|
+
*/
|
|
21
|
+
class ConfigLoader {
|
|
22
|
+
constructor(projectPath) {
|
|
23
|
+
this.projectPath = projectPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
load() {
|
|
27
|
+
const candidates = [
|
|
28
|
+
'cistack.config.js',
|
|
29
|
+
'cistack.config.cjs',
|
|
30
|
+
'cistack.config.mjs',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
const fullPath = path.join(this.projectPath, candidate);
|
|
35
|
+
if (fs.existsSync(fullPath)) {
|
|
36
|
+
try {
|
|
37
|
+
// Clear require cache so hot-reloads work in watch mode
|
|
38
|
+
delete require.cache[require.resolve(fullPath)];
|
|
39
|
+
const cfg = require(fullPath);
|
|
40
|
+
// Handle both `module.exports = {}` and `export default {}`
|
|
41
|
+
const resolved = cfg && cfg.__esModule ? cfg.default : cfg;
|
|
42
|
+
if (resolved && typeof resolved === 'object') {
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`[cistack] Warning: could not load ${candidate}: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Deep-merge config file settings into detected settings.
|
|
56
|
+
* Config file always wins on scalar values; arrays overwrite entirely.
|
|
57
|
+
*/
|
|
58
|
+
static merge(detected, override) {
|
|
59
|
+
if (!override || typeof override !== 'object') return detected;
|
|
60
|
+
|
|
61
|
+
const result = { ...detected };
|
|
62
|
+
|
|
63
|
+
for (const [key, val] of Object.entries(override)) {
|
|
64
|
+
if (val === null || val === undefined) continue;
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(val)) {
|
|
67
|
+
result[key] = val; // arrays overwrite
|
|
68
|
+
} else if (typeof val === 'object' && !Array.isArray(detected[key])) {
|
|
69
|
+
result[key] = { ...(detected[key] || {}), ...val };
|
|
70
|
+
} else {
|
|
71
|
+
result[key] = val;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply config file overrides onto the full detected stack.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} cfg - raw cistack.config.js export
|
|
82
|
+
* @param {object} detected - { hosting, frameworks, languages, testing }
|
|
83
|
+
* @returns {object} - merged config ready for the generator
|
|
84
|
+
*/
|
|
85
|
+
static applyToStack(cfg, detected) {
|
|
86
|
+
if (!cfg || Object.keys(cfg).length === 0) return detected;
|
|
87
|
+
|
|
88
|
+
const result = { ...detected };
|
|
89
|
+
|
|
90
|
+
// Override primary language settings
|
|
91
|
+
if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
|
|
92
|
+
result.languages = result.languages.map((l, i) =>
|
|
93
|
+
i === 0 && (l.name === 'JavaScript' || l.name === 'TypeScript')
|
|
94
|
+
? { ...l, nodeVersion: String(cfg.nodeVersion) }
|
|
95
|
+
: l
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (cfg.packageManager && result.languages && result.languages.length > 0) {
|
|
100
|
+
result.languages = result.languages.map((l, i) =>
|
|
101
|
+
i === 0 ? { ...l, packageManager: cfg.packageManager } : l
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Override hosting
|
|
106
|
+
if (cfg.hosting && Array.isArray(cfg.hosting)) {
|
|
107
|
+
result.hosting = cfg.hosting.map((name) => ({
|
|
108
|
+
name,
|
|
109
|
+
confidence: 1.0,
|
|
110
|
+
manual: true,
|
|
111
|
+
secrets: [],
|
|
112
|
+
notes: ['set via cistack.config.js'],
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Pass through raw extras for generators to consume
|
|
117
|
+
result._config = cfg;
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = ConfigLoader;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects environment variables documented in .env.example or .env.sample.
|
|
8
|
+
*
|
|
9
|
+
* Returns:
|
|
10
|
+
* {
|
|
11
|
+
* secrets: string[], – keys that look like secrets (TOKEN, KEY, SECRET, PASSWORD, PASS)
|
|
12
|
+
* public: string[], – other public env vars
|
|
13
|
+
* all: string[], – full list in file order
|
|
14
|
+
* sourceFile: string, – which file was read
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
class EnvDetector {
|
|
18
|
+
constructor(projectPath, codebaseInfo) {
|
|
19
|
+
this.root = projectPath;
|
|
20
|
+
this.info = codebaseInfo;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async detect() {
|
|
24
|
+
const candidates = ['.env.example', '.env.sample', '.env.template', '.env.defaults'];
|
|
25
|
+
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
const fullPath = path.join(this.root, candidate);
|
|
28
|
+
if (fs.existsSync(fullPath)) {
|
|
29
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
30
|
+
return this._parse(content, candidate);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { secrets: [], public: [], all: [], sourceFile: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_parse(content, sourceFile) {
|
|
38
|
+
const all = [];
|
|
39
|
+
const secrets = [];
|
|
40
|
+
const publicVars = [];
|
|
41
|
+
|
|
42
|
+
const SECRET_PATTERN = /SECRET|TOKEN|KEY|PASSWORD|PASS|PRIVATE|AUTH|CREDENTIAL|CERT|PEM/i;
|
|
43
|
+
|
|
44
|
+
for (const rawLine of content.split('\n')) {
|
|
45
|
+
const line = rawLine.trim();
|
|
46
|
+
|
|
47
|
+
// Skip comments and blank lines
|
|
48
|
+
if (!line || line.startsWith('#')) continue;
|
|
49
|
+
|
|
50
|
+
// Match KEY=value or KEY= or just KEY
|
|
51
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)\s*(?:=.*)?$/i);
|
|
52
|
+
if (!match) continue;
|
|
53
|
+
|
|
54
|
+
const key = match[1].toUpperCase();
|
|
55
|
+
if (all.includes(key)) continue; // de-dupe
|
|
56
|
+
|
|
57
|
+
all.push(key);
|
|
58
|
+
if (SECRET_PATTERN.test(key)) {
|
|
59
|
+
secrets.push(key);
|
|
60
|
+
} else {
|
|
61
|
+
publicVars.push(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { secrets, public: publicVars, all, sourceFile };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = EnvDetector;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects the release tooling used in a project.
|
|
8
|
+
*
|
|
9
|
+
* Checks (in order of priority):
|
|
10
|
+
* 1. semantic-release
|
|
11
|
+
* 2. @changesets/cli
|
|
12
|
+
* 3. release-it
|
|
13
|
+
* 4. standard-version
|
|
14
|
+
*
|
|
15
|
+
* Returns: { tool: string, config: object, publishToNpm: bool } or null
|
|
16
|
+
*/
|
|
17
|
+
class ReleaseDetector {
|
|
18
|
+
constructor(projectPath, codebaseInfo) {
|
|
19
|
+
this.root = projectPath;
|
|
20
|
+
this.info = codebaseInfo;
|
|
21
|
+
this.pkg = codebaseInfo.packageJson || {};
|
|
22
|
+
this.deps = {
|
|
23
|
+
...(this.pkg.dependencies || {}),
|
|
24
|
+
...(this.pkg.devDependencies || {}),
|
|
25
|
+
};
|
|
26
|
+
this.scripts = this.pkg.scripts || {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async detect() {
|
|
30
|
+
// --- semantic-release ---
|
|
31
|
+
if (this.deps['semantic-release']) {
|
|
32
|
+
const config = this._loadSemanticReleaseConfig();
|
|
33
|
+
const plugins = config.plugins || ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/github'];
|
|
34
|
+
return {
|
|
35
|
+
tool: 'semantic-release',
|
|
36
|
+
command: 'npx semantic-release',
|
|
37
|
+
config,
|
|
38
|
+
plugins,
|
|
39
|
+
publishToNpm: plugins.some((p) => (typeof p === 'string' ? p : p[0]) === '@semantic-release/npm'),
|
|
40
|
+
requiresNpmToken: plugins.some((p) => (typeof p === 'string' ? p : p[0]) === '@semantic-release/npm'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- changesets ---
|
|
45
|
+
if (this.deps['@changesets/cli']) {
|
|
46
|
+
const publishScript = this.scripts['release'] || this.scripts['publish'];
|
|
47
|
+
return {
|
|
48
|
+
tool: 'changesets',
|
|
49
|
+
command: 'npx changeset publish',
|
|
50
|
+
publishToNpm: !!(publishScript && publishScript.includes('publish')),
|
|
51
|
+
requiresNpmToken: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- release-it ---
|
|
56
|
+
if (this.deps['release-it']) {
|
|
57
|
+
const config = this._loadReleaseItConfig();
|
|
58
|
+
return {
|
|
59
|
+
tool: 'release-it',
|
|
60
|
+
command: 'npx release-it --ci',
|
|
61
|
+
config,
|
|
62
|
+
publishToNpm: !!(config && config.npm && config.npm.publish !== false),
|
|
63
|
+
requiresNpmToken: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- standard-version ---
|
|
68
|
+
if (this.deps['standard-version']) {
|
|
69
|
+
return {
|
|
70
|
+
tool: 'standard-version',
|
|
71
|
+
command: 'npx standard-version',
|
|
72
|
+
publishToNpm: false,
|
|
73
|
+
requiresNpmToken: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- fallback: check scripts ---
|
|
78
|
+
const releaseScript = this.scripts['release'] || this.scripts['version'];
|
|
79
|
+
if (releaseScript) {
|
|
80
|
+
return {
|
|
81
|
+
tool: 'custom',
|
|
82
|
+
command: 'npm run release',
|
|
83
|
+
publishToNpm: releaseScript.includes('publish'),
|
|
84
|
+
requiresNpmToken: releaseScript.includes('publish'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_loadSemanticReleaseConfig() {
|
|
92
|
+
// Try .releaserc, .releaserc.json, .releaserc.js, package.json#release
|
|
93
|
+
const candidates = ['.releaserc', '.releaserc.json', '.releaserc.js', '.releaserc.yaml'];
|
|
94
|
+
for (const c of candidates) {
|
|
95
|
+
const p = path.join(this.root, c);
|
|
96
|
+
if (fs.existsSync(p)) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
99
|
+
if (c.endsWith('.js')) return require(p);
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (this.pkg.release) return this.pkg.release;
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_loadReleaseItConfig() {
|
|
109
|
+
const candidates = ['.release-it.json', '.release-it.js', '.release-it.yaml'];
|
|
110
|
+
for (const c of candidates) {
|
|
111
|
+
const p = path.join(this.root, c);
|
|
112
|
+
if (fs.existsSync(p)) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
} catch (_) {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (this.pkg['release-it']) return this.pkg['release-it'];
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = ReleaseDetector;
|