@akash-chowdhury-24/deployhub 2.0.21 → 2.0.23
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/package.json +1 -1
- package/src/adapters/dotnet.adapter.js +7 -2
- package/src/adapters/go.adapter.js +7 -2
- package/src/adapters/java.adapter.js +7 -2
- package/src/adapters/node.adapter.js +3 -3
- package/src/adapters/php.adapter.js +7 -2
- package/src/adapters/python.adapter.js +7 -2
- package/src/adapters/rails.adapter.js +11 -4
- package/src/artifact/engine.js +57 -7
- package/src/cli/fatal-error.js +46 -0
- package/src/cli/index.js +9 -1
- package/src/commands/artifact.js +5 -4
- package/src/commands/build.js +3 -9
- package/src/commands/deploy.js +3 -2
- package/src/commands/doctor.js +41 -25
- package/src/commands/env.js +6 -6
- package/src/commands/rollback.js +3 -2
- package/src/commands/storage.js +2 -8
- package/src/commands/sync-k8s-ports.js +3 -2
- package/src/commands/sync-workflows.js +3 -2
- package/src/commands/verify.js +3 -2
- package/src/core/load-config-or-exit.js +37 -0
- package/src/core/stages.js +2 -0
- package/src/deployment/init-prompts.js +31 -1
- package/src/deployment/providers/ssh.js +154 -20
- package/src/detectors/backend.detector.js +16 -1
- package/src/detectors/php.js +3 -2
- package/src/detectors/python.js +3 -2
- package/src/utils/docker-image-deploy.js +9 -1
- package/src/utils/python-app-target.js +247 -0
package/src/commands/verify.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { loadEnv } from '../core/config.js';
|
|
3
|
+
import { loadConfigOrExit } from '../core/load-config-or-exit.js';
|
|
3
4
|
import { resolveEnvTargets } from '../core/environments.js';
|
|
4
5
|
import {
|
|
5
6
|
anyEnvHasResolvableHealthCheckUrl,
|
|
@@ -86,7 +87,7 @@ export function registerVerifyCommand(program) {
|
|
|
86
87
|
)
|
|
87
88
|
.action(async (opts) => {
|
|
88
89
|
loadEnv();
|
|
89
|
-
const config = await
|
|
90
|
+
const config = await loadConfigOrExit();
|
|
90
91
|
|
|
91
92
|
let outcome;
|
|
92
93
|
try {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {unknown} err
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isConfigMissingError(err) {
|
|
9
|
+
return err instanceof Error && /Config not found/i.test(err.message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* User-facing missing-config message (no stack, no snapshot paths).
|
|
14
|
+
*/
|
|
15
|
+
export function printMissingConfigError() {
|
|
16
|
+
console.error(chalk.red('✗ No deployhub.config.json found in this directory.'));
|
|
17
|
+
console.error(chalk.red(" Run 'deployhub init' first to set up your project."));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load config or print a clean error and exit (never throws to the caller).
|
|
22
|
+
* @param {string} [cwd]
|
|
23
|
+
* @returns {Promise<import('./config.js').DeployHubConfig>}
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfigOrExit(cwd = process.cwd()) {
|
|
26
|
+
try {
|
|
27
|
+
return await loadConfig(cwd);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (isConfigMissingError(err)) {
|
|
30
|
+
printMissingConfigError();
|
|
31
|
+
} else {
|
|
32
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/core/stages.js
CHANGED
|
@@ -144,6 +144,8 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// Adapters themselves null-guard buildCommand; this stage always
|
|
148
|
+
// invokes build() so interpreted backends can log the skip cleanly.
|
|
147
149
|
const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
|
|
148
150
|
await adapter.build();
|
|
149
151
|
},
|
|
@@ -24,6 +24,32 @@ export const SERVER_DEPLOY_TYPES = [
|
|
|
24
24
|
|
|
25
25
|
const SSH_BASED = ['ssh', 'ec2', 'azure-vm', 'gcp-vm'];
|
|
26
26
|
|
|
27
|
+
const NODE_PM2_FRAMEWORKS = new Set([
|
|
28
|
+
'express',
|
|
29
|
+
'nestjs',
|
|
30
|
+
'fastify',
|
|
31
|
+
'koa',
|
|
32
|
+
'nextjs',
|
|
33
|
+
'node',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* User-facing label for the backend process identity field (`appName`).
|
|
38
|
+
* Node backends use PM2; other languages use DEPLOYHUB_APP + PID files.
|
|
39
|
+
*
|
|
40
|
+
* @param {string|undefined|null} framework
|
|
41
|
+
* @param {string} projectName
|
|
42
|
+
* @param {'frontend'|'backend'|'both'} projectType
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function backendProcessNamePromptMessage(framework, projectName, projectType) {
|
|
46
|
+
const backendFramework = String(framework || '').toLowerCase();
|
|
47
|
+
const usesPm2 = !backendFramework || NODE_PM2_FRAMEWORKS.has(backendFramework);
|
|
48
|
+
const example = projectType === 'both' ? `${projectName}-api` : projectName;
|
|
49
|
+
return usesPm2
|
|
50
|
+
? `PM2 process name for your backend (e.g. ${example}):`
|
|
51
|
+
: `Process name for your backend (identifies this app's process on the server, e.g. ${example}):`;
|
|
52
|
+
}
|
|
27
53
|
/**
|
|
28
54
|
* Prompt for one environment's deployment method + method-specific config.
|
|
29
55
|
* Shared by `deployhub init` and `deployhub env add`.
|
|
@@ -427,7 +453,11 @@ async function promptSshBasedDeployment(base, projectName, projectType, backendC
|
|
|
427
453
|
questions.push({
|
|
428
454
|
type: 'input',
|
|
429
455
|
name: 'appName',
|
|
430
|
-
message:
|
|
456
|
+
message: backendProcessNamePromptMessage(
|
|
457
|
+
/** @type {string|undefined} */ (backendConfig?.framework),
|
|
458
|
+
projectName,
|
|
459
|
+
projectType
|
|
460
|
+
),
|
|
431
461
|
default: projectType === 'both' ? `${projectName}-api` : projectName,
|
|
432
462
|
});
|
|
433
463
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../../utils/nginx.js';
|
|
13
13
|
import { resolvePm2AppName } from '../../utils/pm2-app-name.js';
|
|
14
14
|
import { shellQuote, formatRemoteCommandFailure } from '../../utils/shell-quote.js';
|
|
15
|
+
import { extractGunicornTarget } from '../../utils/python-app-target.js';
|
|
15
16
|
|
|
16
17
|
/** @type {Set<string>} */
|
|
17
18
|
const NODE_FRAMEWORKS = new Set(['express', 'nestjs', 'fastify', 'koa', 'nextjs', 'node']);
|
|
@@ -54,6 +55,14 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
54
55
|
|
|
55
56
|
const log = createLogger('ssh');
|
|
56
57
|
|
|
58
|
+
// Defense-in-depth: never let a stuck SSH channel hang CI indefinitely.
|
|
59
|
+
// Override with DEPLOYHUB_SSH_EXEC_TIMEOUT_MS (ms). Backend start/stop uses a shorter bound.
|
|
60
|
+
const defaultExecTimeoutMs = Number(env.DEPLOYHUB_SSH_EXEC_TIMEOUT_MS) || 120_000;
|
|
61
|
+
const startStopTimeoutMs = Math.min(
|
|
62
|
+
defaultExecTimeoutMs,
|
|
63
|
+
Number(env.DEPLOYHUB_SSH_START_TIMEOUT_MS) || 60_000
|
|
64
|
+
);
|
|
65
|
+
|
|
57
66
|
async function connect() {
|
|
58
67
|
if (!host || !user) {
|
|
59
68
|
throw new Error(
|
|
@@ -94,10 +103,33 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
94
103
|
/**
|
|
95
104
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
96
105
|
* @param {string} command
|
|
106
|
+
* @param {{ timeoutMs?: number }} [opts]
|
|
97
107
|
*/
|
|
98
|
-
async function exec(ssh, command) {
|
|
108
|
+
async function exec(ssh, command, opts = {}) {
|
|
109
|
+
const timeoutMs = opts.timeoutMs ?? defaultExecTimeoutMs;
|
|
99
110
|
log.info(`$ ${command}`);
|
|
100
|
-
|
|
111
|
+
|
|
112
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
113
|
+
let timer;
|
|
114
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
reject(
|
|
117
|
+
new Error(
|
|
118
|
+
`SSH command timed out after ${timeoutMs}ms on ${user}@${host}. ` +
|
|
119
|
+
`The remote command may still be running — check the server. ` +
|
|
120
|
+
`Command: ${command.length > 240 ? `${command.slice(0, 240)}…` : command}`
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
result = await Promise.race([ssh.execCommand(command), timeoutPromise]);
|
|
129
|
+
} finally {
|
|
130
|
+
if (timer) clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
|
|
101
133
|
if (result.code !== 0 && result.code !== null) {
|
|
102
134
|
const message = formatRemoteCommandFailure(
|
|
103
135
|
command,
|
|
@@ -111,6 +143,47 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
111
143
|
return result;
|
|
112
144
|
}
|
|
113
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Exact marker match in a null-delimited /proc file (cmdline or environ).
|
|
148
|
+
* Uses grep -xF so DEPLOYHUB_APP=myapi does not match DEPLOYHUB_APP=myapi-staging.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} procFileExpr — e.g. `/proc/$pid/environ` or `$proc/cmdline`
|
|
151
|
+
* @param {string} marker — already shell-quoted
|
|
152
|
+
*/
|
|
153
|
+
function procHasExactMarker(procFileExpr, marker) {
|
|
154
|
+
return (
|
|
155
|
+
`tr '\\0' '\\n' < ${procFileExpr} 2>/dev/null | grep -qxF ${marker}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Kill every process whose environ or cmdline contains our exact env/JVM marker.
|
|
161
|
+
* Replaces `pkill -f DEPLOYHUB_APP=…` which only searches cmdline — and
|
|
162
|
+
* `VAR=value nohup cmd` puts the marker in environ only, so orphans from
|
|
163
|
+
* interrupted deploys (no pidfile) were never found.
|
|
164
|
+
*
|
|
165
|
+
* Safe against PID reuse: an unrelated process will not carry our marker.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} markerEnvQ — shell-quoted `DEPLOYHUB_APP=…`
|
|
168
|
+
* @param {string} markerJvmQ — shell-quoted `deployhub.app=…`
|
|
169
|
+
* @param {string} markerJvmFlagQ — shell-quoted `-Ddeployhub.app=…`
|
|
170
|
+
*/
|
|
171
|
+
function killByExactMarkersCmd(markerEnvQ, markerJvmQ, markerJvmFlagQ) {
|
|
172
|
+
return (
|
|
173
|
+
`for proc in /proc/[0-9]*; do ` +
|
|
174
|
+
`pid="\${proc##*/}"; ` +
|
|
175
|
+
`matched=0; ` +
|
|
176
|
+
`if [ -r "$proc/environ" ] && ${procHasExactMarker('"$proc/environ"', markerEnvQ)}; then matched=1; fi; ` +
|
|
177
|
+
`if [ "$matched" -eq 0 ] && [ -r "$proc/cmdline" ]; then ` +
|
|
178
|
+
`if ${procHasExactMarker('"$proc/cmdline"', markerEnvQ)} ` +
|
|
179
|
+
`|| ${procHasExactMarker('"$proc/cmdline"', markerJvmFlagQ)} ` +
|
|
180
|
+
`|| ${procHasExactMarker('"$proc/cmdline"', markerJvmQ)}; then matched=1; fi; ` +
|
|
181
|
+
`fi; ` +
|
|
182
|
+
`if [ "$matched" -eq 1 ]; then kill "$pid" 2>/dev/null || true; fi; ` +
|
|
183
|
+
`done`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
114
187
|
/**
|
|
115
188
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
116
189
|
*/
|
|
@@ -140,7 +213,9 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
140
213
|
* PID-file kill is gated: we only signal a PID if /proc shows our
|
|
141
214
|
* DEPLOYHUB_APP / deployhub.app marker in cmdline or environ. A stale PID
|
|
142
215
|
* reused by an unrelated process is left alone (file still removed).
|
|
143
|
-
*
|
|
216
|
+
*
|
|
217
|
+
* Fallback scans /proc/[pid]/environ (and cmdline) for the exact marker -
|
|
218
|
+
* `pkill -f` cannot see env-only markers from `VAR=value cmd` starts.
|
|
144
219
|
*
|
|
145
220
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
146
221
|
* @param {string} targetPath
|
|
@@ -149,30 +224,75 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
149
224
|
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
150
225
|
const markerEnv = `DEPLOYHUB_APP=${appName}`;
|
|
151
226
|
const markerJvm = `deployhub.app=${appName}`;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
227
|
+
const markerEnvQ = sh(markerEnv);
|
|
228
|
+
const markerJvmQ = sh(markerJvm);
|
|
229
|
+
const markerJvmFlagQ = sh(`-D${markerJvm}`);
|
|
230
|
+
|
|
231
|
+
// Verify-then-kill: only signal a PID if /proc shows our exact marker.
|
|
232
|
+
// Exact (-xF) match so DEPLOYHUB_APP=myapi does not hit myapi-staging.
|
|
155
233
|
await exec(
|
|
156
234
|
ssh,
|
|
157
235
|
`if [ -f ${sh(pidFile)} ]; then ` +
|
|
158
236
|
`pid="$(cat ${sh(pidFile)} 2>/dev/null | tr -cd '0-9')"; ` +
|
|
159
|
-
`if [ -n "$pid" ] && [ -
|
|
160
|
-
`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`
|
|
237
|
+
`if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then ` +
|
|
238
|
+
`matched=0; ` +
|
|
239
|
+
`if [ -r "/proc/$pid/environ" ] && ${procHasExactMarker(`"/proc/$pid/environ"`, markerEnvQ)}; then matched=1; fi; ` +
|
|
240
|
+
`if [ "$matched" -eq 0 ] && [ -r "/proc/$pid/cmdline" ]; then ` +
|
|
241
|
+
`if ${procHasExactMarker(`"/proc/$pid/cmdline"`, markerEnvQ)} ` +
|
|
242
|
+
`|| ${procHasExactMarker(`"/proc/$pid/cmdline"`, markerJvmFlagQ)} ` +
|
|
243
|
+
`|| ${procHasExactMarker(`"/proc/$pid/cmdline"`, markerJvmQ)}; then matched=1; fi; ` +
|
|
164
244
|
`fi; ` +
|
|
245
|
+
`if [ "$matched" -eq 1 ]; then kill "$pid" 2>/dev/null || true; fi; ` +
|
|
165
246
|
`fi; ` +
|
|
166
247
|
`rm -f ${sh(pidFile)}; ` +
|
|
167
|
-
`fi
|
|
248
|
+
`fi`,
|
|
249
|
+
{ timeoutMs: startStopTimeoutMs }
|
|
168
250
|
);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await exec(ssh,
|
|
251
|
+
|
|
252
|
+
// Orphan fallback: no/stale pidfile — find by exact marker in environ or cmdline.
|
|
253
|
+
await exec(ssh, killByExactMarkersCmd(markerEnvQ, markerJvmQ, markerJvmFlagQ), {
|
|
254
|
+
timeoutMs: startStopTimeoutMs,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* After starting a backend, wait briefly and confirm the PID file's process
|
|
260
|
+
* is still alive. Not a health check — only catches immediate crash.
|
|
261
|
+
* `port` is closed over from createSshProvider (settings.port / config.port).
|
|
262
|
+
*
|
|
263
|
+
* @param {import('node-ssh').NodeSSH} ssh
|
|
264
|
+
* @param {string} targetPath
|
|
265
|
+
* @param {string} [logFile] — defaults to targetPath/app.log
|
|
266
|
+
*/
|
|
267
|
+
async function assertPidAliveAfterStart(ssh, targetPath, logFile) {
|
|
268
|
+
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
269
|
+
const log = logFile || `${targetPath}/app.log`;
|
|
270
|
+
const verifyCmd =
|
|
271
|
+
`sleep 2; ` +
|
|
272
|
+
`pid="$(cat ${sh(pidFile)} 2>/dev/null | tr -cd '0-9')"; ` +
|
|
273
|
+
`if [ -z "$pid" ] || [ ! -d "/proc/$pid" ]; then ` +
|
|
274
|
+
`echo "DEPLOYHUB_PROCESS_DIED: process exited immediately after start (pidfile=${sh(pidFile)}). Last lines of ${sh(log)}:"; ` +
|
|
275
|
+
`tail -n 40 ${sh(log)} 2>/dev/null || echo "(no app.log)"; ` +
|
|
276
|
+
`exit 1; ` +
|
|
277
|
+
`fi`;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await exec(ssh, verifyCmd, { timeoutMs: startStopTimeoutMs });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Backend process for "${appName}" died immediately after start at ${targetPath}. ` +
|
|
285
|
+
`Check dependencies, entrypoint, and port ${port}.\n${detail}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
172
288
|
}
|
|
173
289
|
|
|
174
290
|
/**
|
|
175
291
|
* Start a nohup process with DEPLOYHUB_APP marker and write PID file.
|
|
292
|
+
* Marker is set in environ AND embedded as argv0 via `bash exec -a` so it
|
|
293
|
+
* appears in /proc/cmdline (pkill -f / cmdline scans can see it). Plain
|
|
294
|
+
* `VAR=value cmd` alone only puts the marker in environ.
|
|
295
|
+
*
|
|
176
296
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
177
297
|
* @param {string} targetPath
|
|
178
298
|
* @param {string} command — command body after `nohup` (no trailing &)
|
|
@@ -180,10 +300,15 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
180
300
|
async function startScopedNohup(ssh, targetPath, command) {
|
|
181
301
|
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
182
302
|
const dir = sh(targetPath);
|
|
303
|
+
const markerArg = sh(`DEPLOYHUB_APP=${appName}`);
|
|
304
|
+
// stdin from /dev/null + redirects: avoid SSH waiting on leftover FDs.
|
|
305
|
+
// bash exec -a puts DEPLOYHUB_APP=… in argv0 of the real process after exec.
|
|
183
306
|
await exec(
|
|
184
307
|
ssh,
|
|
185
|
-
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} nohup ${command} > app.log 2>&1 & echo $! > ${sh(pidFile)}
|
|
308
|
+
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} nohup bash -c 'exec -a "$0" "$@"' ${markerArg} ${command} > app.log 2>&1 </dev/null & echo $! > ${sh(pidFile)}`,
|
|
309
|
+
{ timeoutMs: startStopTimeoutMs }
|
|
186
310
|
);
|
|
311
|
+
await assertPidAliveAfterStart(ssh, targetPath);
|
|
187
312
|
}
|
|
188
313
|
|
|
189
314
|
/**
|
|
@@ -234,13 +359,22 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
234
359
|
`uvicorn main:app --host 0.0.0.0 --port ${port}`
|
|
235
360
|
);
|
|
236
361
|
} else {
|
|
237
|
-
// gunicorn --daemon writes
|
|
238
|
-
|
|
362
|
+
// gunicorn --daemon writes the master PID to --pid (same .deployhub.pid).
|
|
363
|
+
// --error-logfile + --capture-output give us a log to surface on immediate death
|
|
364
|
+
// (daemonized stdout/stderr otherwise vanish).
|
|
365
|
+
const logFile = `${targetPath}/app.log`;
|
|
366
|
+
const fallbackTarget =
|
|
239
367
|
framework === 'django' ? 'config.wsgi:application' : 'app:app';
|
|
368
|
+
const appTarget =
|
|
369
|
+
extractGunicornTarget(startCommand) || fallbackTarget;
|
|
240
370
|
await exec(
|
|
241
371
|
ssh,
|
|
242
|
-
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget}
|
|
372
|
+
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget} ` +
|
|
373
|
+
`--name ${sh(`deployhub-${appName}`)} --bind 0.0.0.0:${port} ` +
|
|
374
|
+
`--pid ${sh(pidFile)} --error-logfile ${sh(logFile)} --capture-output --daemon`,
|
|
375
|
+
{ timeoutMs: startStopTimeoutMs }
|
|
243
376
|
);
|
|
377
|
+
await assertPidAliveAfterStart(ssh, targetPath, logFile);
|
|
244
378
|
}
|
|
245
379
|
return;
|
|
246
380
|
}
|
|
@@ -261,7 +395,7 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
261
395
|
if (framework === 'spring' || framework === 'java') {
|
|
262
396
|
await stopScopedBackendProcess(ssh, targetPath);
|
|
263
397
|
// -Ddeployhub.app= embeds the env-scoped identity in the JVM command line
|
|
264
|
-
// so
|
|
398
|
+
// so cmdline marker scans and the PID file both target only this env.
|
|
265
399
|
await startScopedNohup(
|
|
266
400
|
ssh,
|
|
267
401
|
targetPath,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
detectDjangoWsgiTarget,
|
|
5
|
+
detectFlaskAppTarget,
|
|
6
|
+
} from '../utils/python-app-target.js';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* @typedef {Object} BackendDetectorResult
|
|
@@ -76,6 +80,7 @@ const FRAMEWORKS = {
|
|
|
76
80
|
defaults: {
|
|
77
81
|
language: 'python',
|
|
78
82
|
buildCommand: null,
|
|
83
|
+
// Fallback only — getBackendInfo overrides via detectDjangoWsgiTarget(cwd)
|
|
79
84
|
startCommand: 'gunicorn config.wsgi:application --bind 0.0.0.0:8000',
|
|
80
85
|
buildOutput: '.',
|
|
81
86
|
testCommand: 'python manage.py test',
|
|
@@ -87,6 +92,7 @@ const FRAMEWORKS = {
|
|
|
87
92
|
defaults: {
|
|
88
93
|
language: 'python',
|
|
89
94
|
buildCommand: null,
|
|
95
|
+
// Fallback only — getBackendInfo overrides via detectFlaskAppTarget(cwd)
|
|
90
96
|
startCommand: 'gunicorn app:app --bind 0.0.0.0:5000',
|
|
91
97
|
buildOutput: '.',
|
|
92
98
|
testCommand: 'pytest',
|
|
@@ -261,6 +267,7 @@ export function getBackendInfo(framework, cwd = process.cwd()) {
|
|
|
261
267
|
let buildCommand = def.defaults.buildCommand;
|
|
262
268
|
let startCommand = def.defaults.startCommand;
|
|
263
269
|
let testCommand = def.defaults.testCommand;
|
|
270
|
+
const port = def.defaults.port;
|
|
264
271
|
|
|
265
272
|
if (def.defaults.language === 'node') {
|
|
266
273
|
if (scripts.build) buildCommand = 'npm run build';
|
|
@@ -268,6 +275,14 @@ export function getBackendInfo(framework, cwd = process.cwd()) {
|
|
|
268
275
|
if (scripts.test) testCommand = 'npm test';
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
if (framework === 'django') {
|
|
279
|
+
const target = detectDjangoWsgiTarget(cwd);
|
|
280
|
+
startCommand = `gunicorn ${target} --bind 0.0.0.0:${port}`;
|
|
281
|
+
} else if (framework === 'flask') {
|
|
282
|
+
const target = detectFlaskAppTarget(cwd);
|
|
283
|
+
startCommand = `gunicorn ${target} --bind 0.0.0.0:${port}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
271
286
|
return {
|
|
272
287
|
projectType: 'backend',
|
|
273
288
|
framework,
|
|
@@ -277,7 +292,7 @@ export function getBackendInfo(framework, cwd = process.cwd()) {
|
|
|
277
292
|
buildOutput: def.defaults.buildOutput,
|
|
278
293
|
testCommand,
|
|
279
294
|
hasDocker,
|
|
280
|
-
port
|
|
295
|
+
port,
|
|
281
296
|
};
|
|
282
297
|
}
|
|
283
298
|
|
package/src/detectors/php.js
CHANGED
|
@@ -6,10 +6,11 @@ function detect(cwd = process.cwd()) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function getInfo(cwd = process.cwd()) {
|
|
9
|
+
// Composer install belongs in the install stage, not a compile/build step.
|
|
9
10
|
return {
|
|
10
11
|
framework: 'php',
|
|
11
|
-
buildCommand:
|
|
12
|
-
buildOutput: '
|
|
12
|
+
buildCommand: null,
|
|
13
|
+
buildOutput: '.',
|
|
13
14
|
hasDocker: fs.existsSync(path.join(cwd, 'Dockerfile')),
|
|
14
15
|
};
|
|
15
16
|
}
|
package/src/detectors/python.js
CHANGED
|
@@ -11,10 +11,11 @@ function detect(cwd = process.cwd()) {
|
|
|
11
11
|
|
|
12
12
|
function getInfo(cwd = process.cwd()) {
|
|
13
13
|
const hasDocker = fs.existsSync(path.join(cwd, 'Dockerfile'));
|
|
14
|
+
// Deps install belongs in the install stage (pip), not a compile/build step.
|
|
14
15
|
return {
|
|
15
16
|
framework: 'python',
|
|
16
|
-
buildCommand:
|
|
17
|
-
buildOutput: '
|
|
17
|
+
buildCommand: null,
|
|
18
|
+
buildOutput: '.',
|
|
18
19
|
hasDocker,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -319,9 +319,17 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
|
|
|
319
319
|
|
|
320
320
|
let reused = false;
|
|
321
321
|
if (!options.skipImageReuse) {
|
|
322
|
+
// Normal deploy: prefer pipeline image (exact tag, then :latest retag).
|
|
322
323
|
reused = await ensureImageFromPipeline(imageRef);
|
|
324
|
+
} else if (await imageExistsLocally(imageRef)) {
|
|
325
|
+
// Rollback: never retag :latest onto an older buildId, but DO use the
|
|
326
|
+
// exact restored buildId image if it is already present locally.
|
|
327
|
+
log.info(`Using restored image ${imageRef} (skipImageReuse — no :latest retag)`);
|
|
328
|
+
reused = true;
|
|
323
329
|
} else {
|
|
324
|
-
log.info(
|
|
330
|
+
log.info(
|
|
331
|
+
`Target image ${imageRef} not found locally — attempting rebuild from artifact`
|
|
332
|
+
);
|
|
325
333
|
}
|
|
326
334
|
|
|
327
335
|
let ranCompose = false;
|