cistack 3.2.0 → 5.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 +183 -124
- package/bin/ciflow.js +3 -2
- package/index.d.ts +91 -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 +66 -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 +330 -99
- package/src/index.js +33 -18
- package/src/utils/helpers.js +21 -7
- package/src/utils/workflow-combiner.js +238 -0
- package/tests/run.js +934 -0
|
@@ -43,17 +43,17 @@ class MonorepoAnalyzer {
|
|
|
43
43
|
|
|
44
44
|
for (const relDir of matches) {
|
|
45
45
|
if (seen.has(relDir)) continue;
|
|
46
|
-
seen.add(relDir);
|
|
47
46
|
|
|
48
47
|
const absPath = path.join(this.root, relDir);
|
|
49
48
|
const pkgJsonPath = path.join(absPath, 'package.json');
|
|
50
|
-
|
|
49
|
+
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
seen.add(relDir);
|
|
52
|
+
|
|
53
|
+
let pkgJson = null;
|
|
54
|
+
try {
|
|
55
|
+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
56
|
+
} catch (_) {}
|
|
57
57
|
|
|
58
58
|
const name = (pkgJson && pkgJson.name) || path.basename(relDir);
|
|
59
59
|
|
|
@@ -25,6 +25,7 @@ class WorkflowAnalyzer {
|
|
|
25
25
|
'docker/build-push-action': 'v5',
|
|
26
26
|
'docker/metadata-action': 'v5',
|
|
27
27
|
'pnpm/action-setup': 'v3',
|
|
28
|
+
'oven-sh/setup-bun': 'v2',
|
|
28
29
|
'codecov/codecov-action': 'v4',
|
|
29
30
|
'github/codeql-action/init': 'v3',
|
|
30
31
|
'github/codeql-action/analyze': 'v3',
|
|
@@ -70,6 +71,15 @@ class WorkflowAnalyzer {
|
|
|
70
71
|
|
|
71
72
|
_auditFile(filename, parsed, rawContent) {
|
|
72
73
|
const issues = [];
|
|
74
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
75
|
+
issues.push({
|
|
76
|
+
type: 'invalid_workflow',
|
|
77
|
+
severity: 'high',
|
|
78
|
+
message: 'Workflow file does not parse to a valid YAML object',
|
|
79
|
+
fix: 'Ensure the workflow root is a YAML mapping with top-level workflow keys',
|
|
80
|
+
});
|
|
81
|
+
return issues;
|
|
82
|
+
}
|
|
73
83
|
|
|
74
84
|
// 1. Check for concurrency
|
|
75
85
|
if (!parsed.concurrency) {
|
|
@@ -85,7 +95,6 @@ class WorkflowAnalyzer {
|
|
|
85
95
|
const actionRegex = /uses:\s*([\w\-\/]+)@([\w\.]+)/g;
|
|
86
96
|
let match;
|
|
87
97
|
while ((match = actionRegex.exec(rawContent)) !== null) {
|
|
88
|
-
const fullAction = match[0];
|
|
89
98
|
const actionName = match[1];
|
|
90
99
|
const currentVersion = match[2];
|
|
91
100
|
|
|
@@ -148,7 +157,6 @@ class WorkflowAnalyzer {
|
|
|
148
157
|
for (const filename of files) {
|
|
149
158
|
const filePath = path.join(this.workflowsDir, filename);
|
|
150
159
|
let content = fs.readFileSync(filePath, 'utf8');
|
|
151
|
-
let originalContent = content;
|
|
152
160
|
let fileChanges = 0;
|
|
153
161
|
|
|
154
162
|
for (const [action, latest] of Object.entries(this.latestVersions)) {
|
package/src/config/loader.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Loads cistack.config.js (or .cjs / .mjs) from the project root.
|
|
@@ -12,8 +13,10 @@ const path = require('path');
|
|
|
12
13
|
* packageManager – override detected PM: 'npm'|'yarn'|'pnpm'|'bun'
|
|
13
14
|
* hosting – array of hosting names to force e.g. ['Firebase']
|
|
14
15
|
* branches – branches to run CI on e.g. ['main', 'staging']
|
|
16
|
+
* (default: detected git default branch, then main/master/develop)
|
|
15
17
|
* cache – { npm: bool, cargo: bool, pip: bool, ... } enable/disable caches
|
|
16
18
|
* monorepo – { perPackage: bool } generate one file per workspace
|
|
19
|
+
* workflowLayout – 'single' | 'split' (default: 'single')
|
|
17
20
|
* release – { tool: 'semantic-release'|'changesets'|'standard-version'|'release-it' }
|
|
18
21
|
* secrets – extra secret names to document in workflow comments
|
|
19
22
|
* outputDir – override default '.github/workflows'
|
|
@@ -23,7 +26,7 @@ class ConfigLoader {
|
|
|
23
26
|
this.projectPath = projectPath;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
load() {
|
|
29
|
+
async load() {
|
|
27
30
|
const candidates = [
|
|
28
31
|
'cistack.config.js',
|
|
29
32
|
'cistack.config.cjs',
|
|
@@ -40,10 +43,11 @@ class ConfigLoader {
|
|
|
40
43
|
// Since this is a CLI, we can afford a bit of hackiness or just support CommonJS primarily.
|
|
41
44
|
|
|
42
45
|
let cfg;
|
|
43
|
-
if (candidate.endsWith('.mjs')) {
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
if (candidate.endsWith('.mjs') || (candidate.endsWith('.js') && !candidate.endsWith('.cjs'))) {
|
|
47
|
+
// Dynamic import() for ESM support
|
|
48
|
+
const modulePath = path.resolve(this.projectPath, candidate);
|
|
49
|
+
const imported = await import(`file://${modulePath}`);
|
|
50
|
+
cfg = imported.default || imported;
|
|
47
51
|
} else {
|
|
48
52
|
delete require.cache[require.resolve(fullPath)];
|
|
49
53
|
cfg = require(fullPath);
|
|
@@ -103,6 +107,30 @@ class ConfigLoader {
|
|
|
103
107
|
if (!cfg || Object.keys(cfg).length === 0) return detected;
|
|
104
108
|
|
|
105
109
|
const result = { ...detected };
|
|
110
|
+
const runScript = (scriptName) => {
|
|
111
|
+
const packageManager =
|
|
112
|
+
(result.languages && result.languages[0] && result.languages[0].packageManager) ||
|
|
113
|
+
cfg.packageManager ||
|
|
114
|
+
'npm';
|
|
115
|
+
if (packageManager === 'yarn') return `yarn run ${scriptName}`;
|
|
116
|
+
if (packageManager === 'pnpm') return `pnpm run ${scriptName}`;
|
|
117
|
+
if (packageManager === 'bun') return `bun run ${scriptName}`;
|
|
118
|
+
return `npm run ${scriptName}`;
|
|
119
|
+
};
|
|
120
|
+
const canonicalHostingNames = {
|
|
121
|
+
firebase: 'Firebase',
|
|
122
|
+
vercel: 'Vercel',
|
|
123
|
+
netlify: 'Netlify',
|
|
124
|
+
aws: 'AWS',
|
|
125
|
+
'gcp app engine': 'GCP App Engine',
|
|
126
|
+
gcp: 'GCP App Engine',
|
|
127
|
+
azure: 'Azure',
|
|
128
|
+
heroku: 'Heroku',
|
|
129
|
+
render: 'Render',
|
|
130
|
+
railway: 'Railway',
|
|
131
|
+
'github pages': 'GitHub Pages',
|
|
132
|
+
'github-pages': 'GitHub Pages',
|
|
133
|
+
};
|
|
106
134
|
|
|
107
135
|
// 1. Language overrides (Node version, package manager)
|
|
108
136
|
if (cfg.nodeVersion && result.languages && result.languages.length > 0) {
|
|
@@ -114,23 +142,41 @@ class ConfigLoader {
|
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
if (cfg.packageManager && result.languages && result.languages.length > 0) {
|
|
117
|
-
result.languages = result.languages.map((l
|
|
118
|
-
|
|
145
|
+
result.languages = result.languages.map((l) =>
|
|
146
|
+
({ ...l, packageManager: cfg.packageManager, manual: true })
|
|
119
147
|
);
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
// 2. Hosting overrides
|
|
123
151
|
if (cfg.hosting) {
|
|
124
152
|
const hostingNames = Array.isArray(cfg.hosting) ? cfg.hosting : [cfg.hosting];
|
|
153
|
+
const hostingSecrets = {
|
|
154
|
+
Firebase: ['FIREBASE_SERVICE_ACCOUNT'],
|
|
155
|
+
Vercel: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
156
|
+
Netlify: ['NETLIFY_AUTH_TOKEN', 'NETLIFY_SITE_ID'],
|
|
157
|
+
AWS: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
|
|
158
|
+
'GCP App Engine': ['GCP_SA_KEY'],
|
|
159
|
+
Azure: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
|
|
160
|
+
Heroku: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
|
|
161
|
+
Render: ['RENDER_DEPLOY_HOOK_URL'],
|
|
162
|
+
Railway: ['RAILWAY_TOKEN'],
|
|
163
|
+
'GitHub Pages': [],
|
|
164
|
+
};
|
|
125
165
|
result.hosting = hostingNames.map((name) => ({
|
|
126
|
-
name,
|
|
166
|
+
name: canonicalHostingNames[String(name).toLowerCase()] || name,
|
|
127
167
|
confidence: 1.0,
|
|
128
168
|
manual: true,
|
|
129
|
-
secrets: [],
|
|
169
|
+
secrets: hostingSecrets[canonicalHostingNames[String(name).toLowerCase()] || name] || [],
|
|
130
170
|
notes: ['set via cistack.config.js'],
|
|
131
171
|
}));
|
|
132
172
|
}
|
|
133
173
|
|
|
174
|
+
// 2b. Release override
|
|
175
|
+
if (cfg.release) {
|
|
176
|
+
const releaseOverride = typeof cfg.release === 'string' ? { tool: cfg.release } : cfg.release;
|
|
177
|
+
result.releaseInfo = { ...(result.releaseInfo || {}), ...releaseOverride };
|
|
178
|
+
}
|
|
179
|
+
|
|
134
180
|
// 3. Framework overrides
|
|
135
181
|
if (cfg.frameworks) {
|
|
136
182
|
const frameworkNames = Array.isArray(cfg.frameworks) ? cfg.frameworks : [cfg.frameworks];
|
|
@@ -149,10 +195,20 @@ class ConfigLoader {
|
|
|
149
195
|
confidence: 1.0,
|
|
150
196
|
manual: true,
|
|
151
197
|
type: 'unit', // default
|
|
152
|
-
command:
|
|
198
|
+
command: runScript('test') // fallback
|
|
153
199
|
}));
|
|
154
200
|
}
|
|
155
201
|
|
|
202
|
+
// 5. Extra documented secrets
|
|
203
|
+
if (cfg.secrets) {
|
|
204
|
+
const extraSecrets = Array.isArray(cfg.secrets) ? cfg.secrets : [cfg.secrets];
|
|
205
|
+
const envVars = result.envVars || { secrets: [], public: [], all: [], sourceFile: null };
|
|
206
|
+
result.envVars = {
|
|
207
|
+
...envVars,
|
|
208
|
+
secrets: [...new Set([...(envVars.secrets || []), ...extraSecrets])],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
156
212
|
// Pass through raw extras for generators to consume
|
|
157
213
|
result._config = { ...(result._config || {}), ...cfg };
|
|
158
214
|
|
|
@@ -17,26 +17,24 @@ class FrameworkDetector {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async detect() {
|
|
20
|
-
const results = [
|
|
21
|
-
|
|
22
|
-
const checks = [
|
|
20
|
+
const results = [
|
|
23
21
|
// JS / TS frontend
|
|
24
|
-
this._check('Next.js', ['next'], ['next.config.js', 'next.config.ts', 'next.config.mjs'], { buildDir: '.next',
|
|
25
|
-
this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.ts'], { buildDir: '.nuxt',
|
|
26
|
-
this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js'], { buildDir: '.svelte-kit',
|
|
27
|
-
this._check('Remix', ['@remix-run/react', '@remix-run/node'], [], {
|
|
28
|
-
this._check('Astro', ['astro'], ['astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist',
|
|
29
|
-
this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.ts'], { buildDir: 'dist' }),
|
|
30
|
-
this._check('React', ['react', 'react-dom'], [], { buildDir: 'build' }),
|
|
31
|
-
this._check('Vue', ['vue'], [], { buildDir: 'dist' }),
|
|
32
|
-
this._check('Angular', ['@angular/core'], [], { buildDir: 'dist',
|
|
33
|
-
this._check('Svelte', ['svelte'], ['svelte.config.js'], { buildDir: 'public' }),
|
|
34
|
-
this._check('Gatsby', ['gatsby'], [], { buildDir: 'public',
|
|
35
|
-
this._check('Ember', ['ember-cli'], [], {}),
|
|
22
|
+
this._check('Next.js', ['next'], ['next.config.js', 'next.config.ts', 'next.config.mjs'], { buildDir: '.next', priority: 10 }),
|
|
23
|
+
this._check('Nuxt', ['nuxt', 'nuxt3'], ['nuxt.config.js', 'nuxt.config.ts'], { buildDir: '.nuxt', priority: 10 }),
|
|
24
|
+
this._check('SvelteKit', ['@sveltejs/kit'], ['svelte.config.js'], { buildDir: '.svelte-kit', priority: 10 }),
|
|
25
|
+
this._check('Remix', ['@remix-run/react', '@remix-run/node'], [], { priority: 10 }),
|
|
26
|
+
this._check('Astro', ['astro'], ['astro.config.mjs', 'astro.config.ts'], { buildDir: 'dist', priority: 10 }),
|
|
27
|
+
this._check('Vite', ['vite'], ['vite.config.js', 'vite.config.ts'], { buildDir: 'dist', priority: 5 }),
|
|
28
|
+
this._check('React', ['react', 'react-dom'], [], { buildDir: 'build', priority: 1 }),
|
|
29
|
+
this._check('Vue', ['vue'], [], { buildDir: 'dist', priority: 1 }),
|
|
30
|
+
this._check('Angular', ['@angular/core'], [], { buildDir: 'dist', priority: 1 }),
|
|
31
|
+
this._check('Svelte', ['svelte'], ['svelte.config.js'], { buildDir: 'public', priority: 1 }),
|
|
32
|
+
this._check('Gatsby', ['gatsby'], [], { buildDir: 'public', priority: 1 }),
|
|
33
|
+
this._check('Ember', ['ember-cli'], [], { priority: 1 }),
|
|
36
34
|
// Node / backend
|
|
37
35
|
this._check('Express', ['express'], [], { isServer: true }),
|
|
38
36
|
this._check('Fastify', ['fastify'], [], { isServer: true }),
|
|
39
|
-
this._check('NestJS', ['@nestjs/core'], [], { isServer: true
|
|
37
|
+
this._check('NestJS', ['@nestjs/core'], [], { isServer: true }),
|
|
40
38
|
this._check('Hono', ['hono'], [], { isServer: true }),
|
|
41
39
|
this._check('Koa', ['koa'], [], { isServer: true }),
|
|
42
40
|
this._check('tRPC', ['@trpc/server', '@trpc/client'], [], { isServer: true }),
|
|
@@ -47,7 +45,7 @@ class FrameworkDetector {
|
|
|
47
45
|
// Ruby
|
|
48
46
|
this._checkRuby('Rails', 'rails'),
|
|
49
47
|
// Java / Kotlin
|
|
50
|
-
this._checkJVM('Spring Boot', 'spring-boot'),
|
|
48
|
+
this._checkJVM('Spring Boot', ['spring-boot', 'org.springframework.boot']),
|
|
51
49
|
// PHP
|
|
52
50
|
this._checkComposer('Laravel', 'laravel/framework'),
|
|
53
51
|
// Go
|
|
@@ -56,8 +54,12 @@ class FrameworkDetector {
|
|
|
56
54
|
this._checkRust('Rust'),
|
|
57
55
|
].filter(Boolean);
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Filter by confidence and priority
|
|
58
|
+
const filtered = results.filter((r) => r.confidence > 0);
|
|
59
|
+
const hasMeta = filtered.some(r => (r.priority || 0) >= 10);
|
|
60
|
+
|
|
61
|
+
return filtered
|
|
62
|
+
.filter(r => !hasMeta || (r.priority || 0) >= 10)
|
|
61
63
|
.sort((a, b) => b.confidence - a.confidence);
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -115,22 +117,24 @@ class FrameworkDetector {
|
|
|
115
117
|
return confidence > 0 ? { name, confidence, isServer: true, isRuby: true, reasons } : null;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
_checkJVM(name,
|
|
120
|
+
_checkJVM(name, keywords) {
|
|
119
121
|
const gradlePath = path.join(this.root, 'build.gradle');
|
|
122
|
+
const gradleKtsPath = path.join(this.root, 'build.gradle.kts');
|
|
120
123
|
const pomPath = path.join(this.root, 'pom.xml');
|
|
121
124
|
let confidence = 0;
|
|
122
125
|
let foundIn = '';
|
|
123
|
-
|
|
126
|
+
const candidates = Array.isArray(keywords) ? keywords : [keywords];
|
|
127
|
+
for (const p of [gradlePath, gradleKtsPath, pomPath]) {
|
|
124
128
|
if (fs.existsSync(p)) {
|
|
125
129
|
const content = fs.readFileSync(p, 'utf8').toLowerCase();
|
|
126
|
-
if (content.includes(keyword.toLowerCase())) {
|
|
127
|
-
confidence = 0.9;
|
|
130
|
+
if (candidates.some((keyword) => content.includes(keyword.toLowerCase()))) {
|
|
131
|
+
confidence = 0.9;
|
|
128
132
|
foundIn = path.basename(p);
|
|
129
|
-
break;
|
|
133
|
+
break;
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
|
-
const reasons = confidence > 0 ? [`found ${
|
|
137
|
+
const reasons = confidence > 0 ? [`found ${name} markers in ${foundIn}`] : [];
|
|
134
138
|
return confidence > 0 ? { name, confidence, isServer: true, isJVM: true, reasons } : null;
|
|
135
139
|
}
|
|
136
140
|
|
package/src/detectors/hosting.js
CHANGED
|
@@ -84,7 +84,7 @@ class HostingDetector {
|
|
|
84
84
|
name: 'Firebase',
|
|
85
85
|
confidence: Math.min(confidence, 1),
|
|
86
86
|
deployCommand: `firebase deploy --only ${deployTarget}`,
|
|
87
|
-
secrets: ['
|
|
87
|
+
secrets: ['FIREBASE_SERVICE_ACCOUNT'],
|
|
88
88
|
reasons,
|
|
89
89
|
buildStep: this._detectBuildScript(),
|
|
90
90
|
};
|
|
@@ -102,7 +102,7 @@ class HostingDetector {
|
|
|
102
102
|
return {
|
|
103
103
|
name: 'Vercel',
|
|
104
104
|
confidence: Math.min(confidence, 1),
|
|
105
|
-
deployCommand: 'vercel --prod
|
|
105
|
+
deployCommand: 'vercel deploy --prod',
|
|
106
106
|
secrets: ['VERCEL_TOKEN', 'VERCEL_ORG_ID', 'VERCEL_PROJECT_ID'],
|
|
107
107
|
reasons,
|
|
108
108
|
buildStep: this._detectBuildScript(),
|
|
@@ -118,7 +118,7 @@ class HostingDetector {
|
|
|
118
118
|
if (this.deps['netlify-cli'] || this.deps['netlify']) { confidence += 0.3; reasons.push('netlify dependency found'); }
|
|
119
119
|
if (Object.values(this.scripts).some((s) => s.includes('netlify'))) { confidence += 0.3; reasons.push('netlify script found'); }
|
|
120
120
|
|
|
121
|
-
let publishDir =
|
|
121
|
+
let publishDir = null;
|
|
122
122
|
try {
|
|
123
123
|
const toml = fs.readFileSync(path.join(this.root, 'netlify.toml'), 'utf8');
|
|
124
124
|
const match = toml.match(/publish\s*=\s*["']?([^"'\n]+)/);
|
|
@@ -173,7 +173,7 @@ class HostingDetector {
|
|
|
173
173
|
name: 'Heroku',
|
|
174
174
|
confidence,
|
|
175
175
|
deployCommand: 'git push heroku main',
|
|
176
|
-
secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME'],
|
|
176
|
+
secrets: ['HEROKU_API_KEY', 'HEROKU_APP_NAME', 'HEROKU_EMAIL'],
|
|
177
177
|
reasons,
|
|
178
178
|
};
|
|
179
179
|
}
|
|
@@ -187,7 +187,7 @@ class HostingDetector {
|
|
|
187
187
|
name: 'GCP App Engine',
|
|
188
188
|
confidence,
|
|
189
189
|
deployCommand: 'gcloud app deploy',
|
|
190
|
-
secrets: ['
|
|
190
|
+
secrets: ['GCP_SA_KEY'],
|
|
191
191
|
reasons,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
@@ -199,11 +199,12 @@ class HostingDetector {
|
|
|
199
199
|
if (this.configs.has('serverless.yml') || this.configs.has('serverless.yaml')) { confidence += 0.6; reasons.push('serverless.yml found'); }
|
|
200
200
|
if (this.configs.has('cdk.json')) { confidence += 0.4; reasons.push('cdk.json found'); }
|
|
201
201
|
if (this.deps['aws-sdk'] || this.deps['@aws-sdk/client-s3']) { confidence += 0.15; reasons.push('aws-sdk found'); }
|
|
202
|
+
const buildDir = this._detectBuildDir();
|
|
202
203
|
return {
|
|
203
204
|
name: 'AWS',
|
|
204
205
|
confidence: Math.min(confidence, 1),
|
|
205
|
-
deployCommand:
|
|
206
|
-
secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
|
|
206
|
+
deployCommand: `aws s3 sync ./${buildDir} s3://$AWS_S3_BUCKET --delete`,
|
|
207
|
+
secrets: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_S3_BUCKET', 'CLOUDFRONT_DISTRIBUTION_ID'],
|
|
207
208
|
reasons,
|
|
208
209
|
};
|
|
209
210
|
}
|
|
@@ -211,13 +212,16 @@ class HostingDetector {
|
|
|
211
212
|
_checkAzure() {
|
|
212
213
|
let confidence = 0;
|
|
213
214
|
const reasons = [];
|
|
214
|
-
if (this.files.has('.azure/pipelines.yml')
|
|
215
|
+
if (this.files.has('.azure/pipelines.yml') || this.files.has('azure/pipelines.yml')) {
|
|
216
|
+
confidence += 0.5;
|
|
217
|
+
reasons.push('azure/pipelines.yml found');
|
|
218
|
+
}
|
|
215
219
|
if (this.deps['@azure/core-http']) { confidence += 0.2; reasons.push('azure core-http found'); }
|
|
216
220
|
return {
|
|
217
221
|
name: 'Azure',
|
|
218
222
|
confidence,
|
|
219
223
|
deployCommand: 'az webapp up',
|
|
220
|
-
secrets: ['
|
|
224
|
+
secrets: ['AZURE_APP_NAME', 'AZURE_WEBAPP_PUBLISH_PROFILE'],
|
|
221
225
|
reasons,
|
|
222
226
|
};
|
|
223
227
|
}
|
|
@@ -242,7 +246,10 @@ class HostingDetector {
|
|
|
242
246
|
_checkDocker() {
|
|
243
247
|
let confidence = 0;
|
|
244
248
|
const reasons = [];
|
|
245
|
-
if (this.configs.has('Dockerfile')
|
|
249
|
+
if (this.configs.has('Dockerfile') || this.configs.has('Dockerfile.prod')) {
|
|
250
|
+
confidence += 0.5;
|
|
251
|
+
reasons.push(this.configs.has('Dockerfile') ? 'Dockerfile found' : 'Dockerfile.prod found');
|
|
252
|
+
}
|
|
246
253
|
if (this.configs.has('docker-compose.yml') || this.configs.has('docker-compose.yaml')) { confidence += 0.3; reasons.push('docker-compose.yml found'); }
|
|
247
254
|
return {
|
|
248
255
|
name: 'Docker',
|
|
@@ -259,6 +266,14 @@ class HostingDetector {
|
|
|
259
266
|
if (scripts['build:prod']) return `npm run build:prod`;
|
|
260
267
|
return null;
|
|
261
268
|
}
|
|
269
|
+
|
|
270
|
+
_detectBuildDir() {
|
|
271
|
+
// If codebase has common build dirs
|
|
272
|
+
const dirs = this.info.srcStructure.topDirs || [];
|
|
273
|
+
if (dirs.includes('dist')) return 'dist';
|
|
274
|
+
if (dirs.includes('build')) return 'build';
|
|
275
|
+
return 'dist'; // fallback
|
|
276
|
+
}
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
module.exports = HostingDetector;
|
|
@@ -78,7 +78,7 @@ class LanguageDetector {
|
|
|
78
78
|
if (lang === 'JavaScript' || lang === 'TypeScript') {
|
|
79
79
|
if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
|
|
80
80
|
if (lockFiles.includes('yarn.lock')) return 'yarn';
|
|
81
|
-
if (lockFiles.includes('bun.lockb')) return 'bun';
|
|
81
|
+
if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
|
|
82
82
|
return 'npm';
|
|
83
83
|
}
|
|
84
84
|
if (lang === 'Python') {
|
|
@@ -89,7 +89,7 @@ class LanguageDetector {
|
|
|
89
89
|
if (lang === 'Ruby') return 'bundler';
|
|
90
90
|
if (lang === 'Go') return 'go mod';
|
|
91
91
|
if (lang === 'Rust') return 'cargo';
|
|
92
|
-
if (lang === 'Java') {
|
|
92
|
+
if (lang === 'Java' || lang === 'Kotlin') {
|
|
93
93
|
if (fs.existsSync(path.join(this.root, 'pom.xml'))) return 'maven';
|
|
94
94
|
return 'gradle';
|
|
95
95
|
}
|
package/src/detectors/release.js
CHANGED
|
@@ -24,6 +24,7 @@ class ReleaseDetector {
|
|
|
24
24
|
...(this.pkg.devDependencies || {}),
|
|
25
25
|
};
|
|
26
26
|
this.scripts = this.pkg.scripts || {};
|
|
27
|
+
this.packageManager = this._detectPackageManager();
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async detect() {
|
|
@@ -55,12 +56,13 @@ class ReleaseDetector {
|
|
|
55
56
|
// --- release-it ---
|
|
56
57
|
if (this.deps['release-it']) {
|
|
57
58
|
const config = this._loadReleaseItConfig();
|
|
59
|
+
const publishToNpm = !!(config && config.npm && config.npm.publish !== false);
|
|
58
60
|
return {
|
|
59
61
|
tool: 'release-it',
|
|
60
62
|
command: 'npx release-it --ci',
|
|
61
63
|
config,
|
|
62
|
-
publishToNpm
|
|
63
|
-
requiresNpmToken:
|
|
64
|
+
publishToNpm,
|
|
65
|
+
requiresNpmToken: publishToNpm,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -77,9 +79,10 @@ class ReleaseDetector {
|
|
|
77
79
|
// --- fallback: check scripts ---
|
|
78
80
|
const releaseScript = this.scripts['release'] || this.scripts['version'];
|
|
79
81
|
if (releaseScript) {
|
|
82
|
+
const scriptName = this.scripts['release'] ? 'release' : 'version';
|
|
80
83
|
return {
|
|
81
84
|
tool: 'custom',
|
|
82
|
-
command:
|
|
85
|
+
command: this._runScript(scriptName),
|
|
83
86
|
publishToNpm: releaseScript.includes('publish'),
|
|
84
87
|
requiresNpmToken: releaseScript.includes('publish'),
|
|
85
88
|
};
|
|
@@ -90,14 +93,15 @@ class ReleaseDetector {
|
|
|
90
93
|
|
|
91
94
|
_loadSemanticReleaseConfig() {
|
|
92
95
|
// Try .releaserc, .releaserc.json, .releaserc.js, package.json#release
|
|
93
|
-
const candidates = ['.releaserc', '.releaserc.json', '.releaserc.js', '.releaserc.yaml'];
|
|
96
|
+
const candidates = ['.releaserc', '.releaserc.json', '.releaserc.js', '.releaserc.cjs', '.releaserc.yaml', '.releaserc.yml'];
|
|
94
97
|
for (const c of candidates) {
|
|
95
98
|
const p = path.join(this.root, c);
|
|
96
99
|
if (fs.existsSync(p)) {
|
|
97
100
|
try {
|
|
101
|
+
if (c.endsWith('.js') || c.endsWith('.cjs')) return require(p);
|
|
98
102
|
const raw = fs.readFileSync(p, 'utf8');
|
|
99
|
-
|
|
100
|
-
return
|
|
103
|
+
const yaml = require('js-yaml');
|
|
104
|
+
return yaml.load(raw);
|
|
101
105
|
} catch (_) {}
|
|
102
106
|
}
|
|
103
107
|
}
|
|
@@ -106,19 +110,36 @@ class ReleaseDetector {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
_loadReleaseItConfig() {
|
|
109
|
-
const candidates = ['.release-it.json', '.release-it.js', '.release-it.yaml'];
|
|
113
|
+
const candidates = ['.release-it.json', '.release-it.js', '.release-it.cjs', '.release-it.yaml', '.release-it.yml'];
|
|
110
114
|
for (const c of candidates) {
|
|
111
115
|
const p = path.join(this.root, c);
|
|
112
116
|
if (fs.existsSync(p)) {
|
|
113
117
|
try {
|
|
118
|
+
if (c.endsWith('.js') || c.endsWith('.cjs')) return require(p);
|
|
114
119
|
const raw = fs.readFileSync(p, 'utf8');
|
|
115
|
-
|
|
120
|
+
const yaml = require('js-yaml');
|
|
121
|
+
return yaml.load(raw);
|
|
116
122
|
} catch (_) {}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
if (this.pkg['release-it']) return this.pkg['release-it'];
|
|
120
126
|
return {};
|
|
121
127
|
}
|
|
128
|
+
|
|
129
|
+
_detectPackageManager() {
|
|
130
|
+
const lockFiles = this.info.lockFiles || [];
|
|
131
|
+
if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
|
|
132
|
+
if (lockFiles.includes('yarn.lock')) return 'yarn';
|
|
133
|
+
if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
|
|
134
|
+
return 'npm';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_runScript(name) {
|
|
138
|
+
if (this.packageManager === 'yarn') return `yarn run ${name}`;
|
|
139
|
+
if (this.packageManager === 'pnpm') return `pnpm run ${name}`;
|
|
140
|
+
if (this.packageManager === 'bun') return `bun run ${name}`;
|
|
141
|
+
return `npm run ${name}`;
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
module.exports = ReleaseDetector;
|
package/src/detectors/testing.js
CHANGED
|
@@ -15,6 +15,7 @@ class TestingDetector {
|
|
|
15
15
|
this.scripts = this.pkg.scripts || {};
|
|
16
16
|
this.configs = new Set(codebaseInfo.configFiles);
|
|
17
17
|
this.files = new Set(codebaseInfo.files);
|
|
18
|
+
this.packageManager = this._detectPackageManager();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async detect() {
|
|
@@ -128,10 +129,25 @@ class TestingDetector {
|
|
|
128
129
|
|
|
129
130
|
_testScript(tool) {
|
|
130
131
|
const scripts = this.pkg.scripts || {};
|
|
131
|
-
if (scripts.test && scripts.test.includes(tool)) return '
|
|
132
|
-
if (scripts['test:ci']) return '
|
|
132
|
+
if (scripts.test && scripts.test.includes(tool)) return this._runScript('test');
|
|
133
|
+
if (scripts['test:ci']) return this._runScript('test:ci');
|
|
133
134
|
return null;
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
_detectPackageManager() {
|
|
138
|
+
const lockFiles = this.info.lockFiles || [];
|
|
139
|
+
if (lockFiles.includes('pnpm-lock.yaml')) return 'pnpm';
|
|
140
|
+
if (lockFiles.includes('yarn.lock')) return 'yarn';
|
|
141
|
+
if (lockFiles.includes('bun.lock') || lockFiles.includes('bun.lockb')) return 'bun';
|
|
142
|
+
return 'npm';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_runScript(name) {
|
|
146
|
+
if (this.packageManager === 'yarn') return `yarn run ${name}`;
|
|
147
|
+
if (this.packageManager === 'pnpm') return `pnpm run ${name}`;
|
|
148
|
+
if (this.packageManager === 'bun') return `bun run ${name}`;
|
|
149
|
+
return `npm run ${name}`;
|
|
150
|
+
}
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
module.exports = TestingDetector;
|
|
@@ -6,7 +6,7 @@ const yaml = require('js-yaml');
|
|
|
6
6
|
* Generates a .github/dependabot.yml based on detected ecosystems.
|
|
7
7
|
*
|
|
8
8
|
* Supported ecosystems:
|
|
9
|
-
* npm, pip, cargo, bundler, go, maven, gradle, github-actions, composer, docker
|
|
9
|
+
* bun, npm, pip, cargo, bundler, go, maven, gradle, github-actions, composer, docker
|
|
10
10
|
*/
|
|
11
11
|
class DependabotGenerator {
|
|
12
12
|
constructor(codebaseInfo) {
|
|
@@ -18,12 +18,32 @@ class DependabotGenerator {
|
|
|
18
18
|
|
|
19
19
|
generate() {
|
|
20
20
|
const updates = [];
|
|
21
|
+
const hasDeps =
|
|
22
|
+
Object.keys(this.pkg.dependencies || {}).length > 0 ||
|
|
23
|
+
Object.keys(this.pkg.devDependencies || {}).length > 0;
|
|
24
|
+
const hasBunLock = this.lockFiles.has('bun.lock');
|
|
25
|
+
|
|
26
|
+
// ── bun ────────────────────────────────────────────────────────────────
|
|
27
|
+
if (hasBunLock) {
|
|
28
|
+
updates.push({
|
|
29
|
+
'package-ecosystem': 'bun',
|
|
30
|
+
directory: '/',
|
|
31
|
+
schedule: { interval: 'weekly', day: 'monday' },
|
|
32
|
+
'open-pull-requests-limit': 10,
|
|
33
|
+
groups: {
|
|
34
|
+
'dev-dependencies': {
|
|
35
|
+
'dependency-type': 'development',
|
|
36
|
+
'update-types': ['minor', 'patch'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
21
41
|
|
|
22
42
|
// ── npm ────────────────────────────────────────────────────────────────
|
|
23
|
-
if (
|
|
43
|
+
if (!hasBunLock && (hasDeps ||
|
|
24
44
|
this.lockFiles.has('package-lock.json') ||
|
|
25
45
|
this.lockFiles.has('yarn.lock') ||
|
|
26
|
-
this.lockFiles.has('pnpm-lock.yaml')) {
|
|
46
|
+
this.lockFiles.has('pnpm-lock.yaml'))) {
|
|
27
47
|
updates.push({
|
|
28
48
|
'package-ecosystem': 'npm',
|
|
29
49
|
directory: '/',
|
|
@@ -112,6 +132,7 @@ class DependabotGenerator {
|
|
|
112
132
|
|
|
113
133
|
// ── Docker ─────────────────────────────────────────────────────────────
|
|
114
134
|
if (this.configFiles.has('Dockerfile') ||
|
|
135
|
+
this.configFiles.has('Dockerfile.prod') ||
|
|
115
136
|
this.configFiles.has('docker-compose.yml') ||
|
|
116
137
|
this.configFiles.has('docker-compose.yaml')) {
|
|
117
138
|
updates.push({
|