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
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
// For .js and .cjs, we can use require (with cache clearing)
|
|
38
|
+
// For .mjs, we might need a different approach, but sticking to sync require for now where possible
|
|
39
|
+
// In a real CLI, we might use dynamic import() but that's async.
|
|
40
|
+
// Since this is a CLI, we can afford a bit of hackiness or just support CommonJS primarily.
|
|
41
|
+
|
|
42
|
+
let cfg;
|
|
43
|
+
if (candidate.endsWith('.mjs')) {
|
|
44
|
+
// Very basic support for .mjs if the environment supports it, but require usually fails.
|
|
45
|
+
// We'll try to use the fact that many environments now support require('.mjs') or just warn.
|
|
46
|
+
cfg = require(fullPath);
|
|
47
|
+
} else {
|
|
48
|
+
delete require.cache[require.resolve(fullPath)];
|
|
49
|
+
cfg = require(fullPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle both `module.exports = {}` and `export default {}`
|
|
53
|
+
const resolved = cfg && cfg.__esModule ? cfg.default : cfg;
|
|
54
|
+
if (resolved && typeof resolved === 'object') {
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// If it fails, it might be because it's ESM.
|
|
59
|
+
// We don't want to crash, but we should inform the user if they have a config but it's broken.
|
|
60
|
+
console.warn(chalk.yellow(`[cistack] Warning: could not load ${candidate}: ${err.message}`));
|
|
61
|
+
if (err.message.includes('ERR_REQUIRE_ESM')) {
|
|
62
|
+
console.warn(chalk.dim(` Tip: Try renaming ${candidate} to ${candidate.replace('.js', '.cjs')} or use CommonJS syntax.`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deep-merge config file settings into detected settings.
|
|
73
|
+
* Config file always wins on scalar values; arrays overwrite entirely.
|
|
74
|
+
*/
|
|
75
|
+
static merge(detected, override) {
|
|
76
|
+
if (!override || typeof override !== 'object') return detected;
|
|
77
|
+
|
|
78
|
+
const result = { ...detected };
|
|
79
|
+
|
|
80
|
+
for (const [key, val] of Object.entries(override)) {
|
|
81
|
+
if (val === null || val === undefined) continue;
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(val)) {
|
|
84
|
+
result[key] = val; // arrays overwrite
|
|
85
|
+
} else if (typeof val === 'object' && !Array.isArray(detected[key])) {
|
|
86
|
+
result[key] = { ...(detected[key] || {}), ...val };
|
|
87
|
+
} else {
|
|
88
|
+
result[key] = val;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Apply config file overrides onto the full detected stack.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} cfg - raw cistack.config.js export
|
|
99
|
+
* @param {object} detected - { hosting, frameworks, languages, testing, ... }
|
|
100
|
+
* @returns {object} - merged config ready for the generator
|
|
101
|
+
*/
|
|
102
|
+
static applyToStack(cfg, detected) {
|
|
103
|
+
if (!cfg || Object.keys(cfg).length === 0) return detected;
|
|
104
|
+
|
|
105
|
+
const result = { ...detected };
|
|
106
|
+
|
|
107
|
+
// 1. Language overrides (Node version, package manager)
|
|
108
|
+
if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
|
|
109
|
+
result.languages = result.languages.map((l, i) =>
|
|
110
|
+
i === 0 && (l.name === 'JavaScript' || l.name === 'TypeScript')
|
|
111
|
+
? { ...l, nodeVersion: String(cfg.nodeVersion), manual: true }
|
|
112
|
+
: l
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (cfg.packageManager && result.languages && result.languages.length > 0) {
|
|
117
|
+
result.languages = result.languages.map((l, i) =>
|
|
118
|
+
i === 0 ? { ...l, packageManager: cfg.packageManager, manual: true } : l
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2. Hosting overrides
|
|
123
|
+
if (cfg.hosting) {
|
|
124
|
+
const hostingNames = Array.isArray(cfg.hosting) ? cfg.hosting : [cfg.hosting];
|
|
125
|
+
result.hosting = hostingNames.map((name) => ({
|
|
126
|
+
name,
|
|
127
|
+
confidence: 1.0,
|
|
128
|
+
manual: true,
|
|
129
|
+
secrets: [],
|
|
130
|
+
notes: ['set via cistack.config.js'],
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Framework overrides
|
|
135
|
+
if (cfg.frameworks) {
|
|
136
|
+
const frameworkNames = Array.isArray(cfg.frameworks) ? cfg.frameworks : [cfg.frameworks];
|
|
137
|
+
result.frameworks = frameworkNames.map(name => ({
|
|
138
|
+
name,
|
|
139
|
+
confidence: 1.0,
|
|
140
|
+
manual: true
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 4. Testing overrides
|
|
145
|
+
if (cfg.testing) {
|
|
146
|
+
const testNames = Array.isArray(cfg.testing) ? cfg.testing : [cfg.testing];
|
|
147
|
+
result.testing = testNames.map(name => ({
|
|
148
|
+
name,
|
|
149
|
+
confidence: 1.0,
|
|
150
|
+
manual: true,
|
|
151
|
+
type: 'unit', // default
|
|
152
|
+
command: `npm run test` // fallback
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Pass through raw extras for generators to consume
|
|
157
|
+
result._config = { ...(result._config || {}), ...cfg };
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
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;
|
|
@@ -64,29 +64,46 @@ class FrameworkDetector {
|
|
|
64
64
|
// ── generic JS/TS checker ─────────────────────────────────────────────────
|
|
65
65
|
_check(name, depKeys, configFiles, meta = {}) {
|
|
66
66
|
let confidence = 0;
|
|
67
|
+
const reasons = [];
|
|
67
68
|
|
|
68
69
|
for (const dep of depKeys) {
|
|
69
|
-
if (this.deps[dep]) {
|
|
70
|
+
if (this.deps[dep]) {
|
|
71
|
+
confidence += 0.5;
|
|
72
|
+
reasons.push(`dependency: ${dep}`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
for (const cfg of configFiles) {
|
|
72
|
-
if (this.configs.has(cfg) || this.files.has(cfg)) {
|
|
77
|
+
if (this.configs.has(cfg) || this.files.has(cfg)) {
|
|
78
|
+
confidence += 0.4;
|
|
79
|
+
reasons.push(`config file: ${cfg}`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
return { name, confidence: Math.min(confidence, 1), ...meta };
|
|
84
|
+
return { name, confidence: Math.min(confidence, 1), reasons, ...meta };
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
_checkPython(name, pkg, markerFile) {
|
|
79
88
|
let confidence = 0;
|
|
89
|
+
const reasons = [];
|
|
80
90
|
const reqFiles = ['requirements.txt', 'Pipfile', 'pyproject.toml'];
|
|
81
91
|
for (const rf of reqFiles) {
|
|
82
92
|
const fullPath = path.join(this.root, rf);
|
|
83
93
|
if (fs.existsSync(fullPath)) {
|
|
84
94
|
const content = fs.readFileSync(fullPath, 'utf8').toLowerCase();
|
|
85
|
-
if (content.includes(pkg.toLowerCase())) {
|
|
95
|
+
if (content.includes(pkg.toLowerCase())) {
|
|
96
|
+
confidence += 0.7;
|
|
97
|
+
reasons.push(`found ${pkg} in ${rf}`);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
|
-
if (markerFile && this.files.has(markerFile))
|
|
89
|
-
|
|
102
|
+
if (markerFile && this.files.has(markerFile)) {
|
|
103
|
+
confidence += 0.2;
|
|
104
|
+
reasons.push(`found marker file ${markerFile}`);
|
|
105
|
+
}
|
|
106
|
+
return confidence > 0 ? { name, confidence: Math.min(confidence, 1), isServer: true, isPython: true, reasons } : null;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
_checkRuby(name, gem) {
|
|
@@ -94,20 +111,27 @@ class FrameworkDetector {
|
|
|
94
111
|
if (!fs.existsSync(gemfilePath)) return null;
|
|
95
112
|
const content = fs.readFileSync(gemfilePath, 'utf8').toLowerCase();
|
|
96
113
|
const confidence = content.includes(gem.toLowerCase()) ? 0.9 : 0;
|
|
97
|
-
|
|
114
|
+
const reasons = confidence > 0 ? [`found ${gem} in Gemfile`] : [];
|
|
115
|
+
return confidence > 0 ? { name, confidence, isServer: true, isRuby: true, reasons } : null;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
_checkJVM(name, keyword) {
|
|
101
119
|
const gradlePath = path.join(this.root, 'build.gradle');
|
|
102
120
|
const pomPath = path.join(this.root, 'pom.xml');
|
|
103
121
|
let confidence = 0;
|
|
122
|
+
let foundIn = '';
|
|
104
123
|
for (const p of [gradlePath, pomPath]) {
|
|
105
124
|
if (fs.existsSync(p)) {
|
|
106
125
|
const content = fs.readFileSync(p, 'utf8').toLowerCase();
|
|
107
|
-
if (content.includes(keyword.toLowerCase())) {
|
|
126
|
+
if (content.includes(keyword.toLowerCase())) {
|
|
127
|
+
confidence = 0.9;
|
|
128
|
+
foundIn = path.basename(p);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
|
-
|
|
133
|
+
const reasons = confidence > 0 ? [`found ${keyword} in ${foundIn}`] : [];
|
|
134
|
+
return confidence > 0 ? { name, confidence, isServer: true, isJVM: true, reasons } : null;
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
_checkComposer(name, pkg) {
|
|
@@ -117,20 +141,21 @@ class FrameworkDetector {
|
|
|
117
141
|
const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
|
|
118
142
|
const allDeps = { ...(composer.require || {}), ...(composer['require-dev'] || {}) };
|
|
119
143
|
const confidence = allDeps[pkg] ? 0.9 : 0;
|
|
120
|
-
|
|
144
|
+
const reasons = confidence > 0 ? [`found ${pkg} in composer.json`] : [];
|
|
145
|
+
return confidence > 0 ? { name, confidence, isServer: true, isPHP: true, reasons } : null;
|
|
121
146
|
} catch (_) { return null; }
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
_checkGo(name) {
|
|
125
150
|
const goMod = path.join(this.root, 'go.mod');
|
|
126
151
|
if (!fs.existsSync(goMod)) return null;
|
|
127
|
-
return { name, confidence: 0.9, isServer: true, isGo: true };
|
|
152
|
+
return { name, confidence: 0.9, isServer: true, isGo: true, reasons: ['go.mod found'] };
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
_checkRust(name) {
|
|
131
156
|
const cargoToml = path.join(this.root, 'Cargo.toml');
|
|
132
157
|
if (!fs.existsSync(cargoToml)) return null;
|
|
133
|
-
return { name, confidence: 0.9, isServer: true, isRust: true };
|
|
158
|
+
return { name, confidence: 0.9, isServer: true, isRust: true, reasons: ['Cargo.toml found'] };
|
|
134
159
|
}
|
|
135
160
|
}
|
|
136
161
|
|
package/src/detectors/hosting.js
CHANGED
|
@@ -60,13 +60,13 @@ class HostingDetector {
|
|
|
60
60
|
|
|
61
61
|
_checkFirebase() {
|
|
62
62
|
let confidence = 0;
|
|
63
|
-
const
|
|
63
|
+
const reasons = [];
|
|
64
64
|
|
|
65
|
-
if (this.configs.has('firebase.json')) { confidence += 0.6;
|
|
66
|
-
if (this.configs.has('.firebaserc')) { confidence += 0.3;
|
|
67
|
-
if (this.deps['firebase-tools'] || this.deps['firebase']) { confidence += 0.2;
|
|
68
|
-
if (Object.values(this.scripts).some((s) => s.includes('firebase deploy'))) { confidence += 0.3;
|
|
69
|
-
if (this.info.srcStructure.hasFunctions) { confidence += 0.1; }
|
|
65
|
+
if (this.configs.has('firebase.json')) { confidence += 0.6; reasons.push('firebase.json found'); }
|
|
66
|
+
if (this.configs.has('.firebaserc')) { confidence += 0.3; reasons.push('.firebaserc found'); }
|
|
67
|
+
if (this.deps['firebase-tools'] || this.deps['firebase']) { confidence += 0.2; reasons.push('firebase dependency found'); }
|
|
68
|
+
if (Object.values(this.scripts).some((s) => s.includes('firebase deploy'))) { confidence += 0.3; reasons.push('firebase deploy script found'); }
|
|
69
|
+
if (this.info.srcStructure.hasFunctions) { confidence += 0.1; reasons.push('functions directory found'); }
|
|
70
70
|
|
|
71
71
|
// Detect what Firebase services are used
|
|
72
72
|
let deployTarget = 'hosting';
|
|
@@ -85,38 +85,38 @@ class HostingDetector {
|
|
|
85
85
|
confidence: Math.min(confidence, 1),
|
|
86
86
|
deployCommand: `firebase deploy --only ${deployTarget}`,
|
|
87
87
|
secrets: ['FIREBASE_TOKEN'],
|
|
88
|
-
|
|
88
|
+
reasons,
|
|
89
89
|
buildStep: this._detectBuildScript(),
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
_checkVercel() {
|
|
94
94
|
let confidence = 0;
|
|
95
|
-
const
|
|
95
|
+
const reasons = [];
|
|
96
96
|
|
|
97
|
-
if (this.configs.has('vercel.json')) { confidence += 0.7;
|
|
98
|
-
if (this.configs.has('.vercel')) { confidence += 0.4;
|
|
99
|
-
if (this.deps['vercel']) { confidence += 0.3;
|
|
100
|
-
if (Object.values(this.scripts).some((s) => s.includes('vercel'))) { confidence += 0.3;
|
|
97
|
+
if (this.configs.has('vercel.json')) { confidence += 0.7; reasons.push('vercel.json found'); }
|
|
98
|
+
if (this.configs.has('.vercel')) { confidence += 0.4; reasons.push('.vercel directory found'); }
|
|
99
|
+
if (this.deps['vercel']) { confidence += 0.3; reasons.push('vercel dependency found'); }
|
|
100
|
+
if (Object.values(this.scripts).some((s) => s.includes('vercel'))) { confidence += 0.3; reasons.push('vercel script found'); }
|
|
101
101
|
|
|
102
102
|
return {
|
|
103
103
|
name: 'Vercel',
|
|
104
104
|
confidence: Math.min(confidence, 1),
|
|
105
105
|
deployCommand: 'vercel --prod --token $VERCEL_TOKEN',
|
|
106
106
|
secrets: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
107
|
-
|
|
107
|
+
reasons,
|
|
108
108
|
buildStep: this._detectBuildScript(),
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
_checkNetlify() {
|
|
113
113
|
let confidence = 0;
|
|
114
|
-
const
|
|
114
|
+
const reasons = [];
|
|
115
115
|
|
|
116
|
-
if (this.configs.has('netlify.toml')) { confidence += 0.7;
|
|
117
|
-
if (this.configs.has('_redirects')) { confidence += 0.2;
|
|
118
|
-
if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3;
|
|
119
|
-
if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3;
|
|
116
|
+
if (this.configs.has('netlify.toml')) { confidence += 0.7; reasons.push('netlify.toml found'); }
|
|
117
|
+
if (this.configs.has('_redirects')) { confidence += 0.2; reasons.push('_redirects file found'); }
|
|
118
|
+
if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3; reasons.push('netlify dependency found'); }
|
|
119
|
+
if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3; reasons.push('netlify script found'); }
|
|
120
120
|
|
|
121
121
|
let publishDir = 'dist';
|
|
122
122
|
try {
|
|
@@ -130,7 +130,7 @@ class HostingDetector {
|
|
|
130
130
|
confidence: Math.min(confidence, 1),
|
|
131
131
|
deployCommand: `netlify deploy --prod --dir=${publishDir}`,
|
|
132
132
|
secrets: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
|
|
133
|
-
|
|
133
|
+
reasons,
|
|
134
134
|
publishDir,
|
|
135
135
|
buildStep: this._detectBuildScript(),
|
|
136
136
|
};
|
|
@@ -138,110 +138,118 @@ class HostingDetector {
|
|
|
138
138
|
|
|
139
139
|
_checkRender() {
|
|
140
140
|
let confidence = 0;
|
|
141
|
-
|
|
141
|
+
const reasons = [];
|
|
142
|
+
if (this.configs.has('render.yaml')) { confidence += 0.8; reasons.push('render.yaml detected'); }
|
|
142
143
|
return {
|
|
143
144
|
name: 'Render',
|
|
144
145
|
confidence,
|
|
145
146
|
deployCommand: 'curl -X POST $RENDER_DEPLOY_HOOK_URL',
|
|
146
147
|
secrets: ['RENDER_DEPLOY_HOOK_URL'],
|
|
147
|
-
|
|
148
|
+
reasons,
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
_checkRailway() {
|
|
152
153
|
let confidence = 0;
|
|
153
|
-
|
|
154
|
-
if (this.
|
|
154
|
+
const reasons = [];
|
|
155
|
+
if (this.configs.has('railway.json') || this.configs.has('railway.toml')) { confidence += 0.8; reasons.push('railway config found'); }
|
|
156
|
+
if (this.deps['@railway/cli']) { confidence += 0.2; reasons.push('railway cli dependency found'); }
|
|
155
157
|
return {
|
|
156
158
|
name: 'Railway',
|
|
157
159
|
confidence,
|
|
158
160
|
deployCommand: 'railway up',
|
|
159
161
|
secrets: ['RAILWAY_TOKEN'],
|
|
160
|
-
|
|
162
|
+
reasons,
|
|
161
163
|
};
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
_checkHeroku() {
|
|
165
167
|
let confidence = 0;
|
|
166
|
-
|
|
167
|
-
if (this.configs.has('
|
|
168
|
-
if (this.
|
|
168
|
+
const reasons = [];
|
|
169
|
+
if (this.configs.has('Procfile')) { confidence += 0.5; reasons.push('Procfile found'); }
|
|
170
|
+
if (this.configs.has('heroku.yml')) { confidence += 0.5; reasons.push('heroku.yml found'); }
|
|
171
|
+
if (this.deps['heroku']) { confidence += 0.2; reasons.push('heroku dependency found'); }
|
|
169
172
|
return {
|
|
170
173
|
name: 'Heroku',
|
|
171
174
|
confidence,
|
|
172
175
|
deployCommand: 'git push heroku main',
|
|
173
176
|
secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME'],
|
|
174
|
-
|
|
177
|
+
reasons,
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
_checkGCPAppEngine() {
|
|
179
182
|
let confidence = 0;
|
|
180
|
-
|
|
181
|
-
if (this.
|
|
183
|
+
const reasons = [];
|
|
184
|
+
if (this.configs.has('app.yaml')) { confidence += 0.7; reasons.push('app.yaml detected'); }
|
|
185
|
+
if (this.deps['@google-cloud/functions-framework']) { confidence += 0.2; reasons.push('gcp functions framework found'); }
|
|
182
186
|
return {
|
|
183
187
|
name: 'GCP App Engine',
|
|
184
188
|
confidence,
|
|
185
189
|
deployCommand: 'gcloud app deploy',
|
|
186
190
|
secrets: ['GCP_PROJECT_ID', 'GCP_SA_KEY'],
|
|
187
|
-
|
|
191
|
+
reasons,
|
|
188
192
|
};
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
_checkAWS() {
|
|
192
196
|
let confidence = 0;
|
|
193
|
-
|
|
194
|
-
if (this.configs.has('
|
|
195
|
-
if (this.configs.has('
|
|
196
|
-
if (this.
|
|
197
|
+
const reasons = [];
|
|
198
|
+
if (this.configs.has('appspec.yml')) { confidence += 0.5; reasons.push('appspec.yml found'); }
|
|
199
|
+
if (this.configs.has('serverless.yml') || this.configs.has('serverless.yaml')) { confidence += 0.6; reasons.push('serverless.yml found'); }
|
|
200
|
+
if (this.configs.has('cdk.json')) { confidence += 0.4; reasons.push('cdk.json found'); }
|
|
201
|
+
if (this.deps['aws-sdk'] || this.deps['@aws-sdk/client-s3']) { confidence += 0.15; reasons.push('aws-sdk found'); }
|
|
197
202
|
return {
|
|
198
203
|
name: 'AWS',
|
|
199
204
|
confidence: Math.min(confidence, 1),
|
|
200
205
|
deployCommand: 'aws s3 sync ./dist s3://$AWS_S3_BUCKET --delete',
|
|
201
206
|
secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
|
|
202
|
-
|
|
207
|
+
reasons,
|
|
203
208
|
};
|
|
204
209
|
}
|
|
205
210
|
|
|
206
211
|
_checkAzure() {
|
|
207
212
|
let confidence = 0;
|
|
208
|
-
|
|
209
|
-
if (this.
|
|
213
|
+
const reasons = [];
|
|
214
|
+
if (this.files.has('.azure/pipelines.yml')) { confidence += 0.5; reasons.push('.azure/pipelines.yml found'); }
|
|
215
|
+
if (this.deps['@azure/core-http']) { confidence += 0.2; reasons.push('azure core-http found'); }
|
|
210
216
|
return {
|
|
211
217
|
name: 'Azure',
|
|
212
218
|
confidence,
|
|
213
219
|
deployCommand: 'az webapp up',
|
|
214
220
|
secrets: ['AZURE_CREDENTIALS'],
|
|
215
|
-
|
|
221
|
+
reasons,
|
|
216
222
|
};
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
_checkGitHubPages() {
|
|
220
226
|
let confidence = 0;
|
|
227
|
+
const reasons = [];
|
|
221
228
|
const pkgHomepage = this.pkg.homepage || '';
|
|
222
|
-
if (pkgHomepage.includes('github.io')) { confidence += 0.6; }
|
|
223
|
-
if (this.deps['gh-pages']) { confidence += 0.4; }
|
|
224
|
-
if (Object.values(this.scripts).some((s) => s.includes('gh-pages'))) confidence += 0.3;
|
|
229
|
+
if (pkgHomepage.includes('github.io')) { confidence += 0.6; reasons.push('homepage contains github.io'); }
|
|
230
|
+
if (this.deps['gh-pages']) { confidence += 0.4; reasons.push('gh-pages dependency found'); }
|
|
231
|
+
if (Object.values(this.scripts).some((s) => s.includes('gh-pages'))) { confidence += 0.3; reasons.push('gh-pages script found'); }
|
|
225
232
|
return {
|
|
226
233
|
name: 'GitHub Pages',
|
|
227
234
|
confidence: Math.min(confidence, 1),
|
|
228
235
|
deployCommand: null, // handled by actions/deploy-pages
|
|
229
236
|
secrets: [],
|
|
230
|
-
|
|
237
|
+
reasons,
|
|
231
238
|
buildStep: this._detectBuildScript(),
|
|
232
239
|
};
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
_checkDocker() {
|
|
236
243
|
let confidence = 0;
|
|
237
|
-
|
|
238
|
-
if (this.configs.has('
|
|
244
|
+
const reasons = [];
|
|
245
|
+
if (this.configs.has('Dockerfile')) { confidence += 0.5; reasons.push('Dockerfile found'); }
|
|
246
|
+
if (this.configs.has('docker-compose.yml') || this.configs.has('docker-compose.yaml')) { confidence += 0.3; reasons.push('docker-compose.yml found'); }
|
|
239
247
|
return {
|
|
240
248
|
name: 'Docker',
|
|
241
249
|
confidence,
|
|
242
250
|
deployCommand: 'docker push $DOCKER_IMAGE',
|
|
243
251
|
secrets: ['DOCKER_USERNAME', 'DOCKER_PASSWORD'],
|
|
244
|
-
|
|
252
|
+
reasons,
|
|
245
253
|
};
|
|
246
254
|
}
|
|
247
255
|
|