@akash-chowdhury-24/deployhub 2.0.26 → 2.0.27
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 +33 -18
- package/package.json +1 -1
- package/src/adapters/index.js +1 -0
- package/src/artifact/engine.js +2 -2
- package/src/commands/doctor.js +8 -4
- package/src/commands/init.js +5 -0
- package/src/deployment/deployment-env.js +115 -41
- package/src/deployment/init-prompts.js +52 -7
- package/src/deployment/providers/azure-vm.js +6 -4
- package/src/deployment/providers/ec2.js +8 -3
- package/src/deployment/providers/gcp-vm.js +4 -4
- package/src/deployment/providers/ssh.js +2 -2
- package/src/detectors/backend.detector.js +107 -2
- package/src/detectors/index.js +3 -2
- package/src/utils/credential-inventory.js +292 -0
- package/src/utils/docker-image.js +6 -3
- package/src/utils/dockerfile-expose.js +1 -0
- package/src/utils/dockerfile.js +181 -28
- package/src/utils/github-actions.js +67 -10
|
@@ -11,9 +11,11 @@ import { getEnvSettings } from '../../core/config.js';
|
|
|
11
11
|
export function createAzureVmProvider(config, envName, env = process.env) {
|
|
12
12
|
const log = createLogger('azure-vm');
|
|
13
13
|
const settings = getEnvSettings(config.environments[envName]);
|
|
14
|
-
const subscriptionId =
|
|
15
|
-
|
|
16
|
-
const
|
|
14
|
+
const subscriptionId =
|
|
15
|
+
env.AZURE_VM_LOOKUP_SUBSCRIPTION_ID || settings.azureSubscriptionId;
|
|
16
|
+
const resourceGroup =
|
|
17
|
+
env.AZURE_VM_LOOKUP_RESOURCE_GROUP || settings.azureResourceGroup;
|
|
18
|
+
const vmName = env.AZURE_VM_LOOKUP_VM_NAME || settings.azureVmName;
|
|
17
19
|
|
|
18
20
|
async function resolveHost() {
|
|
19
21
|
if (settings.host || env.SSH_HOST) {
|
|
@@ -23,7 +25,7 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
23
25
|
if (!subscriptionId || !resourceGroup || !vmName) {
|
|
24
26
|
throw new Error(
|
|
25
27
|
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — ' +
|
|
26
|
-
'provide SSH_HOST (VM public IP/DNS) or set
|
|
28
|
+
'provide SSH_HOST (VM public IP/DNS) or set AZURE_VM_LOOKUP_SUBSCRIPTION_ID, AZURE_VM_LOOKUP_RESOURCE_GROUP, and AZURE_VM_LOOKUP_VM_NAME for auto lookup.'
|
|
27
29
|
);
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -12,7 +12,8 @@ export function createEc2Provider(config, envName, env = process.env) {
|
|
|
12
12
|
const log = createLogger('ec2');
|
|
13
13
|
const settings = getEnvSettings(config.environments[envName]);
|
|
14
14
|
const instanceId = env.EC2_INSTANCE_ID || settings.ec2InstanceId;
|
|
15
|
-
const region =
|
|
15
|
+
const region =
|
|
16
|
+
env.EC2_LOOKUP_AWS_REGION || settings.awsRegion || 'us-east-1';
|
|
16
17
|
|
|
17
18
|
async function resolveHost() {
|
|
18
19
|
if (settings.host || env.SSH_HOST) {
|
|
@@ -30,8 +31,12 @@ export function createEc2Provider(config, envName, env = process.env) {
|
|
|
30
31
|
|
|
31
32
|
/** @type {Record<string, string>} */
|
|
32
33
|
const awsEnv = { ...process.env };
|
|
33
|
-
if (env.
|
|
34
|
-
|
|
34
|
+
if (env.EC2_LOOKUP_AWS_ACCESS_KEY_ID) {
|
|
35
|
+
awsEnv.AWS_ACCESS_KEY_ID = env.EC2_LOOKUP_AWS_ACCESS_KEY_ID;
|
|
36
|
+
}
|
|
37
|
+
if (env.EC2_LOOKUP_AWS_SECRET_ACCESS_KEY) {
|
|
38
|
+
awsEnv.AWS_SECRET_ACCESS_KEY = env.EC2_LOOKUP_AWS_SECRET_ACCESS_KEY;
|
|
39
|
+
}
|
|
35
40
|
awsEnv.AWS_DEFAULT_REGION = region;
|
|
36
41
|
|
|
37
42
|
try {
|
|
@@ -11,7 +11,7 @@ import { getEnvSettings } from '../../core/config.js';
|
|
|
11
11
|
export function createGcpVmProvider(config, envName, env = process.env) {
|
|
12
12
|
const log = createLogger('gcp-vm');
|
|
13
13
|
const settings = getEnvSettings(config.environments[envName]);
|
|
14
|
-
const projectId = env.
|
|
14
|
+
const projectId = env.GCP_VM_LOOKUP_PROJECT_ID || settings.gcpProjectId;
|
|
15
15
|
const zone = env.GCP_ZONE || settings.gcpZone;
|
|
16
16
|
const instanceName = env.GCP_INSTANCE_NAME || settings.gcpInstanceName;
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ export function createGcpVmProvider(config, envName, env = process.env) {
|
|
|
23
23
|
if (!projectId || !zone || !instanceName) {
|
|
24
24
|
throw new Error(
|
|
25
25
|
'Could not resolve host via GCP instance lookup, and no SSH_HOST was set — ' +
|
|
26
|
-
'provide SSH_HOST (instance external IP/DNS) or set
|
|
26
|
+
'provide SSH_HOST (instance external IP/DNS) or set GCP_VM_LOOKUP_PROJECT_ID, GCP_ZONE, and GCP_INSTANCE_NAME for auto lookup.'
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -31,8 +31,8 @@ export function createGcpVmProvider(config, envName, env = process.env) {
|
|
|
31
31
|
|
|
32
32
|
/** @type {Record<string, string>} */
|
|
33
33
|
const gcpEnv = { ...process.env };
|
|
34
|
-
if (env.
|
|
35
|
-
gcpEnv.GOOGLE_APPLICATION_CREDENTIALS = env.
|
|
34
|
+
if (env.GCP_VM_LOOKUP_KEY_FILE) {
|
|
35
|
+
gcpEnv.GOOGLE_APPLICATION_CREDENTIALS = env.GCP_VM_LOOKUP_KEY_FILE;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
try {
|
|
@@ -426,10 +426,10 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
426
426
|
return;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
if (framework === 'rails') {
|
|
429
|
+
if (framework === 'rails' || framework === 'ruby') {
|
|
430
430
|
await exec(ssh, `cd ${dir} && bundle install --deployment`);
|
|
431
431
|
await stopScopedBackendProcess(ssh, targetPath);
|
|
432
|
-
await startScopedNohup(ssh, targetPath, `bundle exec puma -
|
|
432
|
+
await startScopedNohup(ssh, targetPath, `bundle exec puma -b tcp://0.0.0.0:${port}`);
|
|
433
433
|
return;
|
|
434
434
|
}
|
|
435
435
|
|
|
@@ -64,6 +64,20 @@ const FRAMEWORKS = {
|
|
|
64
64
|
port: 3000,
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
+
node: {
|
|
68
|
+
// After named Node frameworks: package.json backend without Express/Nest/Fastify/Koa.
|
|
69
|
+
// Exclude obvious frontend-only trees so React/Vue apps are not labeled "node".
|
|
70
|
+
detect: (cwd) =>
|
|
71
|
+
fs.existsSync(path.join(cwd, 'package.json')) && !isLikelyFrontendPackage(cwd),
|
|
72
|
+
defaults: {
|
|
73
|
+
language: 'node',
|
|
74
|
+
buildCommand: null,
|
|
75
|
+
startCommand: 'npm start',
|
|
76
|
+
buildOutput: '.',
|
|
77
|
+
testCommand: 'npm test',
|
|
78
|
+
port: 3000,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
67
81
|
fastapi: {
|
|
68
82
|
detect: (cwd) => fileContains(cwd, 'requirements.txt', 'fastapi'),
|
|
69
83
|
defaults: {
|
|
@@ -99,6 +113,20 @@ const FRAMEWORKS = {
|
|
|
99
113
|
port: 5000,
|
|
100
114
|
},
|
|
101
115
|
},
|
|
116
|
+
python: {
|
|
117
|
+
detect: (cwd) =>
|
|
118
|
+
fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
|
119
|
+
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
120
|
+
fs.existsSync(path.join(cwd, 'setup.py')),
|
|
121
|
+
defaults: {
|
|
122
|
+
language: 'python',
|
|
123
|
+
buildCommand: null,
|
|
124
|
+
startCommand: 'python -m http.server 8000 --bind 0.0.0.0',
|
|
125
|
+
buildOutput: '.',
|
|
126
|
+
testCommand: 'pytest',
|
|
127
|
+
port: 8000,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
102
130
|
laravel: {
|
|
103
131
|
detect: (cwd) => hasComposerPackage(cwd, 'laravel/framework'),
|
|
104
132
|
defaults: {
|
|
@@ -115,12 +143,25 @@ const FRAMEWORKS = {
|
|
|
115
143
|
defaults: {
|
|
116
144
|
language: 'php',
|
|
117
145
|
buildCommand: null,
|
|
118
|
-
startCommand: 'php-
|
|
146
|
+
startCommand: 'php -S 0.0.0.0:80 -t public',
|
|
119
147
|
buildOutput: '.',
|
|
120
148
|
testCommand: 'php bin/phpunit',
|
|
121
149
|
port: 80,
|
|
122
150
|
},
|
|
123
151
|
},
|
|
152
|
+
php: {
|
|
153
|
+
// After Laravel/Symfony: composer.json without those frameworks, or bare .php sources.
|
|
154
|
+
detect: (cwd) =>
|
|
155
|
+
fs.existsSync(path.join(cwd, 'composer.json')) || hasPhpSources(cwd),
|
|
156
|
+
defaults: {
|
|
157
|
+
language: 'php',
|
|
158
|
+
buildCommand: null,
|
|
159
|
+
startCommand: 'php -S 0.0.0.0:80 -t .',
|
|
160
|
+
buildOutput: '.',
|
|
161
|
+
testCommand: null,
|
|
162
|
+
port: 80,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
124
165
|
spring: {
|
|
125
166
|
detect: (cwd) => fileContains(cwd, 'pom.xml', 'spring-boot'),
|
|
126
167
|
defaults: {
|
|
@@ -132,6 +173,20 @@ const FRAMEWORKS = {
|
|
|
132
173
|
port: 8080,
|
|
133
174
|
},
|
|
134
175
|
},
|
|
176
|
+
java: {
|
|
177
|
+
detect: (cwd) =>
|
|
178
|
+
fs.existsSync(path.join(cwd, 'pom.xml')) ||
|
|
179
|
+
fs.existsSync(path.join(cwd, 'build.gradle')) ||
|
|
180
|
+
fs.existsSync(path.join(cwd, 'build.gradle.kts')),
|
|
181
|
+
defaults: {
|
|
182
|
+
language: 'java',
|
|
183
|
+
buildCommand: 'mvn package',
|
|
184
|
+
startCommand: 'java -jar target/*.jar',
|
|
185
|
+
buildOutput: 'target',
|
|
186
|
+
testCommand: 'mvn test',
|
|
187
|
+
port: 8080,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
135
190
|
go: {
|
|
136
191
|
detect: (cwd) => fs.existsSync(path.join(cwd, 'go.mod')),
|
|
137
192
|
defaults: {
|
|
@@ -165,12 +220,23 @@ const FRAMEWORKS = {
|
|
|
165
220
|
defaults: {
|
|
166
221
|
language: 'ruby',
|
|
167
222
|
buildCommand: 'bundle exec rake assets:precompile',
|
|
168
|
-
startCommand: 'bundle exec puma',
|
|
223
|
+
startCommand: 'bundle exec puma -b tcp://0.0.0.0:3000',
|
|
169
224
|
buildOutput: '.',
|
|
170
225
|
testCommand: 'bundle exec rspec',
|
|
171
226
|
port: 3000,
|
|
172
227
|
},
|
|
173
228
|
},
|
|
229
|
+
ruby: {
|
|
230
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, 'Gemfile')),
|
|
231
|
+
defaults: {
|
|
232
|
+
language: 'ruby',
|
|
233
|
+
buildCommand: null,
|
|
234
|
+
startCommand: 'bundle exec puma -b tcp://0.0.0.0:9292',
|
|
235
|
+
buildOutput: '.',
|
|
236
|
+
testCommand: 'bundle exec rspec',
|
|
237
|
+
port: 9292,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
174
240
|
};
|
|
175
241
|
|
|
176
242
|
const DETECTION_ORDER = [
|
|
@@ -178,15 +244,20 @@ const DETECTION_ORDER = [
|
|
|
178
244
|
'express',
|
|
179
245
|
'fastify',
|
|
180
246
|
'koa',
|
|
247
|
+
'node',
|
|
181
248
|
'fastapi',
|
|
182
249
|
'django',
|
|
183
250
|
'flask',
|
|
251
|
+
'python',
|
|
184
252
|
'laravel',
|
|
185
253
|
'symfony',
|
|
254
|
+
'php',
|
|
186
255
|
'spring',
|
|
256
|
+
'java',
|
|
187
257
|
'go',
|
|
188
258
|
'dotnet',
|
|
189
259
|
'rails',
|
|
260
|
+
'ruby',
|
|
190
261
|
];
|
|
191
262
|
|
|
192
263
|
/**
|
|
@@ -201,6 +272,22 @@ function hasPackageDep(cwd, dep) {
|
|
|
201
272
|
return !!deps[dep];
|
|
202
273
|
}
|
|
203
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @param {string} cwd
|
|
277
|
+
* @returns {boolean}
|
|
278
|
+
*/
|
|
279
|
+
function isLikelyFrontendPackage(cwd) {
|
|
280
|
+
return [
|
|
281
|
+
'react',
|
|
282
|
+
'vue',
|
|
283
|
+
'@angular/core',
|
|
284
|
+
'svelte',
|
|
285
|
+
'astro',
|
|
286
|
+
'next',
|
|
287
|
+
'vite',
|
|
288
|
+
].some((dep) => hasPackageDep(cwd, dep));
|
|
289
|
+
}
|
|
290
|
+
|
|
204
291
|
/**
|
|
205
292
|
* @param {string} cwd
|
|
206
293
|
* @param {string} filename
|
|
@@ -225,6 +312,24 @@ function hasComposerPackage(cwd, packageName) {
|
|
|
225
312
|
return !!deps[packageName];
|
|
226
313
|
}
|
|
227
314
|
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} cwd
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
function hasPhpSources(cwd) {
|
|
320
|
+
try {
|
|
321
|
+
const root = fs.readdirSync(cwd);
|
|
322
|
+
if (root.some((f) => f.endsWith('.php'))) return true;
|
|
323
|
+
const publicDir = path.join(cwd, 'public');
|
|
324
|
+
if (fs.existsSync(publicDir) && fs.statSync(publicDir).isDirectory()) {
|
|
325
|
+
return fs.readdirSync(publicDir).some((f) => f.endsWith('.php'));
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
228
333
|
/**
|
|
229
334
|
* @param {string} [cwd]
|
|
230
335
|
* @returns {string|null}
|
package/src/detectors/index.js
CHANGED
|
@@ -43,13 +43,14 @@ export async function detectFramework(cwd = process.cwd()) {
|
|
|
43
43
|
for (const detector of legacyDetectors) {
|
|
44
44
|
if (detector.detect(cwd)) {
|
|
45
45
|
const info = detector.getInfo(cwd);
|
|
46
|
+
const backendLegacy = new Set(['node', 'php', 'python', 'java', 'go', 'dotnet', 'ruby']);
|
|
46
47
|
return {
|
|
47
48
|
...info,
|
|
48
|
-
projectType: info.framework
|
|
49
|
+
projectType: backendLegacy.has(info.framework) ? 'backend' : 'frontend',
|
|
49
50
|
language: info.framework,
|
|
50
51
|
startCommand: null,
|
|
51
52
|
testCommand: 'npm test',
|
|
52
|
-
port: 3000,
|
|
53
|
+
port: info.framework === 'php' ? 80 : info.framework === 'ruby' ? 9292 : 3000,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical credential / env-var inventory for storage × deployment collision audits.
|
|
3
|
+
*
|
|
4
|
+
* Storage lists are the COMPLETE runtime reads from src/storage/providers/*.js
|
|
5
|
+
* (plus schema keys from PROVIDER_ENV_MAP). Deployment lists come from
|
|
6
|
+
* DEPLOYMENT_ENV_DEFS / DEPLOYMENT_ENV_KEYS (the declared complete method sets).
|
|
7
|
+
*
|
|
8
|
+
* Keep this module in sync when adding providers or methods — the matrix test
|
|
9
|
+
* fails if storage and deployment declare overlapping bare names.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DEPLOYMENT_ENV_KEYS } from '../deployment/deployment-env.js';
|
|
13
|
+
|
|
14
|
+
/** Storage provider ids in matrix order. */
|
|
15
|
+
export const STORAGE_PROVIDER_ORDER = [
|
|
16
|
+
'aws',
|
|
17
|
+
'azure',
|
|
18
|
+
'gcp',
|
|
19
|
+
'gdrive',
|
|
20
|
+
'dropbox',
|
|
21
|
+
'ftp',
|
|
22
|
+
'local',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Deployment method ids in matrix order. */
|
|
26
|
+
export const DEPLOYMENT_METHOD_ORDER = [
|
|
27
|
+
'ssh',
|
|
28
|
+
'ec2',
|
|
29
|
+
'azure-vm',
|
|
30
|
+
'gcp-vm',
|
|
31
|
+
'docker',
|
|
32
|
+
'kubernetes',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Complete env vars each storage provider reads at runtime.
|
|
37
|
+
* Local: no credentials — DEPLOYHUB_LOCAL_STORAGE_DIR is an optional test pin only
|
|
38
|
+
* (listed under runtimeExtras, not as a credential).
|
|
39
|
+
*
|
|
40
|
+
* @type {Record<string, string[]>}
|
|
41
|
+
*/
|
|
42
|
+
export const STORAGE_RUNTIME_ENV_KEYS = {
|
|
43
|
+
aws: [
|
|
44
|
+
'AWS_ACCESS_KEY_ID',
|
|
45
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
46
|
+
'AWS_BUCKET',
|
|
47
|
+
'AWS_REGION',
|
|
48
|
+
],
|
|
49
|
+
azure: ['AZURE_CONNECTION_STRING', 'AZURE_CONTAINER'],
|
|
50
|
+
gcp: ['GCP_PROJECT_ID', 'GCP_KEY_FILE', 'GCP_BUCKET'],
|
|
51
|
+
gdrive: [
|
|
52
|
+
'GDRIVE_CLIENT_ID',
|
|
53
|
+
'GDRIVE_CLIENT_SECRET',
|
|
54
|
+
'GDRIVE_REFRESH_TOKEN',
|
|
55
|
+
'GDRIVE_FOLDER_ID',
|
|
56
|
+
],
|
|
57
|
+
dropbox: ['DROPBOX_ACCESS_TOKEN'],
|
|
58
|
+
ftp: ['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD', 'FTP_PORT', 'FTP_PATH'],
|
|
59
|
+
local: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Non-credential runtime extras (optional paths / tuning). Not treated as
|
|
64
|
+
* storage credentials for collision purposes, but tracked for schema-drift docs.
|
|
65
|
+
*
|
|
66
|
+
* @type {Record<string, string[]>}
|
|
67
|
+
*/
|
|
68
|
+
export const STORAGE_RUNTIME_EXTRAS = {
|
|
69
|
+
local: ['DEPLOYHUB_LOCAL_STORAGE_DIR'],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Complete declared deployment method env keys (core + optional + CI).
|
|
74
|
+
* @type {Record<string, string[]>}
|
|
75
|
+
*/
|
|
76
|
+
export const DEPLOYMENT_RUNTIME_ENV_KEYS = {
|
|
77
|
+
ssh: DEPLOYMENT_ENV_KEYS.ssh,
|
|
78
|
+
ec2: DEPLOYMENT_ENV_KEYS.ec2,
|
|
79
|
+
'azure-vm': DEPLOYMENT_ENV_KEYS['azure-vm'],
|
|
80
|
+
'gcp-vm': DEPLOYMENT_ENV_KEYS['gcp-vm'],
|
|
81
|
+
docker: DEPLOYMENT_ENV_KEYS.docker,
|
|
82
|
+
kubernetes: DEPLOYMENT_ENV_KEYS.kubernetes,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* SSH runtime also reads these tuning knobs (not in DEPLOYMENT_ENV_DEFS).
|
|
87
|
+
* Not credentials; listed for inventory completeness.
|
|
88
|
+
*/
|
|
89
|
+
export const SSH_RUNTIME_TUNING_KEYS = [
|
|
90
|
+
'DEPLOYHUB_SSH_EXEC_TIMEOUT_MS',
|
|
91
|
+
'DEPLOYHUB_SSH_START_TIMEOUT_MS',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Intentional same-name sharing across deployment methods (same concern).
|
|
96
|
+
* Separated per-environment by the existing secret-prefixing system when
|
|
97
|
+
* multiple environments exist.
|
|
98
|
+
*
|
|
99
|
+
* @type {{ methods: [string, string], keys: string[], reason: string }[]}
|
|
100
|
+
*/
|
|
101
|
+
export const SHARED_BY_DESIGN_DEPLOY_PAIRS = [
|
|
102
|
+
{
|
|
103
|
+
methods: ['ssh', 'ec2'],
|
|
104
|
+
keys: [
|
|
105
|
+
'SSH_HOST',
|
|
106
|
+
'SSH_USER',
|
|
107
|
+
'SSH_KEY_PATH',
|
|
108
|
+
'SSH_SSH_PORT',
|
|
109
|
+
'SSH_DEPLOY_PATH',
|
|
110
|
+
'SSH_APP_NAME',
|
|
111
|
+
'SSH_PORT',
|
|
112
|
+
'SSH_KEY',
|
|
113
|
+
],
|
|
114
|
+
reason: 'EC2 wraps SSH after optional host lookup — same SSH concern',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
methods: ['ssh', 'azure-vm'],
|
|
118
|
+
keys: [
|
|
119
|
+
'SSH_HOST',
|
|
120
|
+
'SSH_USER',
|
|
121
|
+
'SSH_KEY_PATH',
|
|
122
|
+
'SSH_SSH_PORT',
|
|
123
|
+
'SSH_DEPLOY_PATH',
|
|
124
|
+
'SSH_APP_NAME',
|
|
125
|
+
'SSH_PORT',
|
|
126
|
+
'SSH_KEY',
|
|
127
|
+
],
|
|
128
|
+
reason: 'Azure VM wraps SSH after optional host lookup — same SSH concern',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
methods: ['ssh', 'gcp-vm'],
|
|
132
|
+
keys: [
|
|
133
|
+
'SSH_HOST',
|
|
134
|
+
'SSH_USER',
|
|
135
|
+
'SSH_KEY_PATH',
|
|
136
|
+
'SSH_SSH_PORT',
|
|
137
|
+
'SSH_DEPLOY_PATH',
|
|
138
|
+
'SSH_APP_NAME',
|
|
139
|
+
'SSH_PORT',
|
|
140
|
+
'SSH_KEY',
|
|
141
|
+
],
|
|
142
|
+
reason: 'GCP VM wraps SSH after optional host lookup — same SSH concern',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
methods: ['ec2', 'azure-vm'],
|
|
146
|
+
keys: [
|
|
147
|
+
'SSH_HOST',
|
|
148
|
+
'SSH_USER',
|
|
149
|
+
'SSH_KEY_PATH',
|
|
150
|
+
'SSH_SSH_PORT',
|
|
151
|
+
'SSH_DEPLOY_PATH',
|
|
152
|
+
'SSH_APP_NAME',
|
|
153
|
+
'SSH_PORT',
|
|
154
|
+
'SSH_KEY',
|
|
155
|
+
],
|
|
156
|
+
reason: 'Both SSH-based cloud VMs share the SSH credential namespace',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
methods: ['ec2', 'gcp-vm'],
|
|
160
|
+
keys: [
|
|
161
|
+
'SSH_HOST',
|
|
162
|
+
'SSH_USER',
|
|
163
|
+
'SSH_KEY_PATH',
|
|
164
|
+
'SSH_SSH_PORT',
|
|
165
|
+
'SSH_DEPLOY_PATH',
|
|
166
|
+
'SSH_APP_NAME',
|
|
167
|
+
'SSH_PORT',
|
|
168
|
+
'SSH_KEY',
|
|
169
|
+
],
|
|
170
|
+
reason: 'Both SSH-based cloud VMs share the SSH credential namespace',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
methods: ['azure-vm', 'gcp-vm'],
|
|
174
|
+
keys: [
|
|
175
|
+
'SSH_HOST',
|
|
176
|
+
'SSH_USER',
|
|
177
|
+
'SSH_KEY_PATH',
|
|
178
|
+
'SSH_SSH_PORT',
|
|
179
|
+
'SSH_DEPLOY_PATH',
|
|
180
|
+
'SSH_APP_NAME',
|
|
181
|
+
'SSH_PORT',
|
|
182
|
+
'SSH_KEY',
|
|
183
|
+
],
|
|
184
|
+
reason: 'Both SSH-based cloud VMs share the SSH credential namespace',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
methods: ['docker', 'kubernetes'],
|
|
188
|
+
keys: [
|
|
189
|
+
'DOCKER_IMAGE_NAME',
|
|
190
|
+
'DOCKER_IMAGE_TAG',
|
|
191
|
+
'DOCKER_REGISTRY_URL',
|
|
192
|
+
'DOCKER_REGISTRY_USERNAME',
|
|
193
|
+
'DOCKER_REGISTRY_TOKEN',
|
|
194
|
+
],
|
|
195
|
+
reason:
|
|
196
|
+
'Same container-registry concern — Kubernetes builds/pushes via the same Docker image helpers',
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string[]} a
|
|
202
|
+
* @param {string[]} b
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
export function intersectKeys(a, b) {
|
|
206
|
+
const setB = new Set(b);
|
|
207
|
+
return a.filter((k) => setB.has(k)).sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Every storage × deployment overlap cell.
|
|
212
|
+
* Empty array = no overlap.
|
|
213
|
+
*
|
|
214
|
+
* @returns {Record<string, Record<string, string[]>>}
|
|
215
|
+
*/
|
|
216
|
+
export function buildStorageDeployOverlapMatrix() {
|
|
217
|
+
/** @type {Record<string, Record<string, string[]>>} */
|
|
218
|
+
const matrix = {};
|
|
219
|
+
for (const storageId of STORAGE_PROVIDER_ORDER) {
|
|
220
|
+
matrix[storageId] = {};
|
|
221
|
+
const storageKeys = STORAGE_RUNTIME_ENV_KEYS[storageId] || [];
|
|
222
|
+
for (const method of DEPLOYMENT_METHOD_ORDER) {
|
|
223
|
+
const deployKeys = DEPLOYMENT_RUNTIME_ENV_KEYS[method] || [];
|
|
224
|
+
matrix[storageId][method] = intersectKeys(storageKeys, deployKeys);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return matrix;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Flatten all unintended storage×deploy collisions (should be empty).
|
|
232
|
+
* @returns {{ storage: string, method: string, keys: string[] }[]}
|
|
233
|
+
*/
|
|
234
|
+
export function listStorageDeployCollisions() {
|
|
235
|
+
const matrix = buildStorageDeployOverlapMatrix();
|
|
236
|
+
/** @type {{ storage: string, method: string, keys: string[] }[]} */
|
|
237
|
+
const hits = [];
|
|
238
|
+
for (const storageId of STORAGE_PROVIDER_ORDER) {
|
|
239
|
+
for (const method of DEPLOYMENT_METHOD_ORDER) {
|
|
240
|
+
const keys = matrix[storageId][method];
|
|
241
|
+
if (keys.length > 0) {
|
|
242
|
+
hits.push({ storage: storageId, method, keys });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return hits;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Deploy×deploy shared keys, classified against SHARED_BY_DESIGN_DEPLOY_PAIRS.
|
|
251
|
+
* @returns {{ a: string, b: string, keys: string[], designed: boolean, reason?: string }[]}
|
|
252
|
+
*/
|
|
253
|
+
export function listDeployDeployShares() {
|
|
254
|
+
/** @type {{ a: string, b: string, keys: string[], designed: boolean, reason?: string }[]} */
|
|
255
|
+
const rows = [];
|
|
256
|
+
for (let i = 0; i < DEPLOYMENT_METHOD_ORDER.length; i++) {
|
|
257
|
+
for (let j = i + 1; j < DEPLOYMENT_METHOD_ORDER.length; j++) {
|
|
258
|
+
const a = DEPLOYMENT_METHOD_ORDER[i];
|
|
259
|
+
const b = DEPLOYMENT_METHOD_ORDER[j];
|
|
260
|
+
const keys = intersectKeys(
|
|
261
|
+
DEPLOYMENT_RUNTIME_ENV_KEYS[a] || [],
|
|
262
|
+
DEPLOYMENT_RUNTIME_ENV_KEYS[b] || []
|
|
263
|
+
);
|
|
264
|
+
if (keys.length === 0) continue;
|
|
265
|
+
const designed = SHARED_BY_DESIGN_DEPLOY_PAIRS.find(
|
|
266
|
+
(p) =>
|
|
267
|
+
(p.methods[0] === a && p.methods[1] === b) ||
|
|
268
|
+
(p.methods[0] === b && p.methods[1] === a)
|
|
269
|
+
);
|
|
270
|
+
const unexpected = designed
|
|
271
|
+
? keys.filter((k) => !designed.keys.includes(k))
|
|
272
|
+
: keys;
|
|
273
|
+
rows.push({
|
|
274
|
+
a,
|
|
275
|
+
b,
|
|
276
|
+
keys,
|
|
277
|
+
designed: Boolean(designed) && unexpected.length === 0,
|
|
278
|
+
reason: designed?.reason,
|
|
279
|
+
});
|
|
280
|
+
if (designed && unexpected.length > 0) {
|
|
281
|
+
rows.push({
|
|
282
|
+
a,
|
|
283
|
+
b,
|
|
284
|
+
keys: unexpected,
|
|
285
|
+
designed: false,
|
|
286
|
+
reason: `unexpected share beyond designed set for ${a}∩${b}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return rows;
|
|
292
|
+
}
|
|
@@ -207,6 +207,9 @@ export function isInterpretedBackendFramework(framework) {
|
|
|
207
207
|
'flask',
|
|
208
208
|
'laravel',
|
|
209
209
|
'symfony',
|
|
210
|
+
'php',
|
|
211
|
+
'python',
|
|
212
|
+
'ruby',
|
|
210
213
|
'rails',
|
|
211
214
|
].includes(framework || '');
|
|
212
215
|
}
|
|
@@ -224,21 +227,21 @@ export function describeInterpretedBackendGap(framework) {
|
|
|
224
227
|
installCmd: 'npm ci',
|
|
225
228
|
};
|
|
226
229
|
}
|
|
227
|
-
if (['fastapi', 'django', 'flask'].includes(framework)) {
|
|
230
|
+
if (['fastapi', 'django', 'flask', 'python'].includes(framework)) {
|
|
228
231
|
return {
|
|
229
232
|
ecosystem: 'Python',
|
|
230
233
|
missing: 'a pre-installed virtualenv / site-packages (artifacts only ship requirements.txt + source)',
|
|
231
234
|
installCmd: 'pip install',
|
|
232
235
|
};
|
|
233
236
|
}
|
|
234
|
-
if (['laravel', 'symfony'].includes(framework)) {
|
|
237
|
+
if (['laravel', 'symfony', 'php'].includes(framework)) {
|
|
235
238
|
return {
|
|
236
239
|
ecosystem: 'PHP',
|
|
237
240
|
missing: 'vendor/ (Composer dependencies are not packaged into the artifact)',
|
|
238
241
|
installCmd: 'composer install',
|
|
239
242
|
};
|
|
240
243
|
}
|
|
241
|
-
if (framework === 'rails') {
|
|
244
|
+
if (framework === 'rails' || framework === 'ruby') {
|
|
242
245
|
return {
|
|
243
246
|
ecosystem: 'Ruby',
|
|
244
247
|
missing: 'vendor/bundle / installed gems (artifacts only ship Gemfile + lock)',
|
|
@@ -71,6 +71,7 @@ export function resolveFallbackContainerPort(config) {
|
|
|
71
71
|
if (framework === 'go') return 8080;
|
|
72
72
|
if (framework === 'dotnet') return 5000;
|
|
73
73
|
if (framework === 'rails') return 3000;
|
|
74
|
+
if (framework === 'ruby') return 9292;
|
|
74
75
|
|
|
75
76
|
// nextjs, nestjs, express, and other Node backends
|
|
76
77
|
return 3000;
|