@akash-chowdhury-24/deployhub 2.0.20 → 2.0.22
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 +47 -3
- package/install.ps1 +142 -17
- package/install.sh +81 -3
- 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 +48 -3
- 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
|
@@ -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']);
|
|
@@ -171,8 +172,43 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
171
172
|
await exec(ssh, `pkill -f ${sh(markerJvm)} || true`);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* After starting a backend, wait briefly and confirm the PID file's process
|
|
177
|
+
* is still alive. Not a health check — only catches immediate crash.
|
|
178
|
+
* `port` is closed over from createSshProvider (settings.port / config.port).
|
|
179
|
+
*
|
|
180
|
+
* @param {import('node-ssh').NodeSSH} ssh
|
|
181
|
+
* @param {string} targetPath
|
|
182
|
+
* @param {string} [logFile] — defaults to targetPath/app.log
|
|
183
|
+
*/
|
|
184
|
+
async function assertPidAliveAfterStart(ssh, targetPath, logFile) {
|
|
185
|
+
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
186
|
+
const log = logFile || `${targetPath}/app.log`;
|
|
187
|
+
const verifyCmd =
|
|
188
|
+
`sleep 2; ` +
|
|
189
|
+
`pid="$(cat ${sh(pidFile)} 2>/dev/null | tr -cd '0-9')"; ` +
|
|
190
|
+
`if [ -z "$pid" ] || [ ! -d "/proc/$pid" ]; then ` +
|
|
191
|
+
`echo "DEPLOYHUB_PROCESS_DIED: process exited immediately after start (pidfile=${sh(pidFile)}). Last lines of ${sh(log)}:"; ` +
|
|
192
|
+
`tail -n 40 ${sh(log)} 2>/dev/null || echo "(no app.log)"; ` +
|
|
193
|
+
`exit 1; ` +
|
|
194
|
+
`fi`;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await exec(ssh, verifyCmd);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Backend process for "${appName}" died immediately after start at ${targetPath}. ` +
|
|
202
|
+
`Check dependencies, entrypoint, and port ${port}.\n${detail}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
174
207
|
/**
|
|
175
208
|
* Start a nohup process with DEPLOYHUB_APP marker and write PID file.
|
|
209
|
+
* After launch, wait briefly and confirm the PID is still alive — otherwise
|
|
210
|
+
* surface app.log and fail the deploy (nohup+echo $! alone always "succeeds").
|
|
211
|
+
*
|
|
176
212
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
177
213
|
* @param {string} targetPath
|
|
178
214
|
* @param {string} command — command body after `nohup` (no trailing &)
|
|
@@ -184,6 +220,7 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
184
220
|
ssh,
|
|
185
221
|
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} nohup ${command} > app.log 2>&1 & echo $! > ${sh(pidFile)}`
|
|
186
222
|
);
|
|
223
|
+
await assertPidAliveAfterStart(ssh, targetPath);
|
|
187
224
|
}
|
|
188
225
|
|
|
189
226
|
/**
|
|
@@ -234,13 +271,21 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
234
271
|
`uvicorn main:app --host 0.0.0.0 --port ${port}`
|
|
235
272
|
);
|
|
236
273
|
} else {
|
|
237
|
-
// gunicorn --daemon writes
|
|
238
|
-
|
|
274
|
+
// gunicorn --daemon writes the master PID to --pid (same .deployhub.pid).
|
|
275
|
+
// --error-logfile + --capture-output give us a log to surface on immediate death
|
|
276
|
+
// (daemonized stdout/stderr otherwise vanish).
|
|
277
|
+
const logFile = `${targetPath}/app.log`;
|
|
278
|
+
const fallbackTarget =
|
|
239
279
|
framework === 'django' ? 'config.wsgi:application' : 'app:app';
|
|
280
|
+
const appTarget =
|
|
281
|
+
extractGunicornTarget(startCommand) || fallbackTarget;
|
|
240
282
|
await exec(
|
|
241
283
|
ssh,
|
|
242
|
-
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget}
|
|
284
|
+
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget} ` +
|
|
285
|
+
`--name ${sh(`deployhub-${appName}`)} --bind 0.0.0.0:${port} ` +
|
|
286
|
+
`--pid ${sh(pidFile)} --error-logfile ${sh(logFile)} --capture-output --daemon`
|
|
243
287
|
);
|
|
288
|
+
await assertPidAliveAfterStart(ssh, targetPath, logFile);
|
|
244
289
|
}
|
|
245
290
|
return;
|
|
246
291
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/** Directories to skip when walking for wsgi.py / Flask entrypoints. */
|
|
5
|
+
const SKIP_DIRS = new Set([
|
|
6
|
+
'venv',
|
|
7
|
+
'.venv',
|
|
8
|
+
'env',
|
|
9
|
+
'.env',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'__pycache__',
|
|
12
|
+
'.git',
|
|
13
|
+
'.tox',
|
|
14
|
+
'site-packages',
|
|
15
|
+
'dist',
|
|
16
|
+
'build',
|
|
17
|
+
'.eggs',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} startCommand
|
|
22
|
+
* @returns {string|null} e.g. `myapp.wsgi:application` or `app:app`
|
|
23
|
+
*/
|
|
24
|
+
export function extractGunicornTarget(startCommand) {
|
|
25
|
+
if (!startCommand || typeof startCommand !== 'string') return null;
|
|
26
|
+
const tokens = startCommand.trim().split(/\s+/);
|
|
27
|
+
const gIdx = tokens.findIndex((t) => t === 'gunicorn' || t.endsWith('/gunicorn'));
|
|
28
|
+
if (gIdx < 0) return null;
|
|
29
|
+
for (let i = gIdx + 1; i < tokens.length; i++) {
|
|
30
|
+
const t = tokens[i];
|
|
31
|
+
if (t.startsWith('-')) {
|
|
32
|
+
// skip flag and its value when it looks like `--bind 0.0.0.0:8000`
|
|
33
|
+
if (
|
|
34
|
+
t === '-b' ||
|
|
35
|
+
t === '--bind' ||
|
|
36
|
+
t === '-c' ||
|
|
37
|
+
t === '--config' ||
|
|
38
|
+
t === '-n' ||
|
|
39
|
+
t === '--name' ||
|
|
40
|
+
t === '-p' ||
|
|
41
|
+
t === '--pid' ||
|
|
42
|
+
t === '-w' ||
|
|
43
|
+
t === '--workers' ||
|
|
44
|
+
t === '--chdir' ||
|
|
45
|
+
t === '-e' ||
|
|
46
|
+
t === '--env' ||
|
|
47
|
+
t === '--error-logfile' ||
|
|
48
|
+
t === '--access-logfile' ||
|
|
49
|
+
t === '--log-file'
|
|
50
|
+
) {
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// gunicorn app target is module:callable
|
|
56
|
+
if (/^[A-Za-z_][\w.]*:[A-Za-z_]\w*$/.test(t)) {
|
|
57
|
+
return t;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Walk cwd for files named `wsgi.py`, skipping venvs etc.
|
|
65
|
+
* @param {string} cwd
|
|
66
|
+
* @param {number} [maxDepth]
|
|
67
|
+
* @returns {string[]} absolute paths
|
|
68
|
+
*/
|
|
69
|
+
function findWsgiFiles(cwd, maxDepth = 4) {
|
|
70
|
+
/** @type {string[]} */
|
|
71
|
+
const found = [];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} dir
|
|
75
|
+
* @param {number} depth
|
|
76
|
+
*/
|
|
77
|
+
function walk(dir, depth) {
|
|
78
|
+
if (depth > maxDepth || found.length >= 20) return;
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const ent of entries) {
|
|
86
|
+
if (ent.name.startsWith('.') && ent.name !== '.venv') {
|
|
87
|
+
// skip hidden except we already skip .venv via SKIP_DIRS
|
|
88
|
+
if (ent.isDirectory()) continue;
|
|
89
|
+
}
|
|
90
|
+
const full = path.join(dir, ent.name);
|
|
91
|
+
if (ent.isDirectory()) {
|
|
92
|
+
if (SKIP_DIRS.has(ent.name)) continue;
|
|
93
|
+
walk(full, depth + 1);
|
|
94
|
+
} else if (ent.isFile() && ent.name === 'wsgi.py') {
|
|
95
|
+
found.push(full);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
walk(cwd, 0);
|
|
101
|
+
return found;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert absolute wsgi.py path under cwd to gunicorn target `pkg.wsgi:application`.
|
|
106
|
+
* @param {string} cwd
|
|
107
|
+
* @param {string} wsgiAbs
|
|
108
|
+
* @returns {string|null}
|
|
109
|
+
*/
|
|
110
|
+
export function wsgiPathToGunicornTarget(cwd, wsgiAbs) {
|
|
111
|
+
const rel = path.relative(cwd, wsgiAbs);
|
|
112
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
113
|
+
const noExt = rel.replace(/\.py$/i, '');
|
|
114
|
+
const parts = noExt.split(/[/\\]/).filter(Boolean);
|
|
115
|
+
if (parts.length === 0) return null;
|
|
116
|
+
// Invalid if any segment is not a Python identifier
|
|
117
|
+
if (parts.some((p) => !/^[A-Za-z_]\w*$/.test(p))) return null;
|
|
118
|
+
return `${parts.join('.')}:application`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Package directory containing wsgi.py relative to cwd (e.g. `myapp`), or null if root wsgi.py.
|
|
123
|
+
* @param {string} cwd
|
|
124
|
+
* @returns {string|null} relative dir name, or '' for root-level wsgi.py, or null if none
|
|
125
|
+
*/
|
|
126
|
+
export function detectDjangoWsgiPackageDir(cwd) {
|
|
127
|
+
const wsgiAbs = pickPreferredWsgiFile(cwd);
|
|
128
|
+
if (!wsgiAbs) return null;
|
|
129
|
+
const rel = path.relative(cwd, path.dirname(wsgiAbs));
|
|
130
|
+
if (!rel || rel === '.') return '';
|
|
131
|
+
// Only copy the top-level package segment (myapp/wsgi.py → myapp)
|
|
132
|
+
const top = rel.split(/[/\\]/)[0];
|
|
133
|
+
return top || '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Prefer standard startproject layout (one level down from manage.py), then config/, then any.
|
|
138
|
+
* @param {string} cwd
|
|
139
|
+
* @returns {string|null} absolute path to wsgi.py
|
|
140
|
+
*/
|
|
141
|
+
function pickPreferredWsgiFile(cwd) {
|
|
142
|
+
const files = findWsgiFiles(cwd);
|
|
143
|
+
if (files.length === 0) return null;
|
|
144
|
+
|
|
145
|
+
const hasManage = fs.existsSync(path.join(cwd, 'manage.py'));
|
|
146
|
+
|
|
147
|
+
// Prefer <pkg>/wsgi.py directly under cwd when manage.py exists (django-admin startproject)
|
|
148
|
+
if (hasManage) {
|
|
149
|
+
const oneLevel = files.filter((f) => {
|
|
150
|
+
const rel = path.relative(cwd, f);
|
|
151
|
+
const parts = rel.split(/[/\\]/);
|
|
152
|
+
return parts.length === 2 && parts[1] === 'wsgi.py';
|
|
153
|
+
});
|
|
154
|
+
if (oneLevel.length === 1) return oneLevel[0];
|
|
155
|
+
// Prefer config/wsgi.py when present among one-level candidates (cookiecutter)
|
|
156
|
+
const configOne = oneLevel.find((f) => path.basename(path.dirname(f)) === 'config');
|
|
157
|
+
if (configOne) return configOne;
|
|
158
|
+
if (oneLevel.length > 0) return oneLevel[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const configWsgi = files.find((f) => {
|
|
162
|
+
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
163
|
+
return rel === 'config/wsgi.py';
|
|
164
|
+
});
|
|
165
|
+
if (configWsgi) return configWsgi;
|
|
166
|
+
|
|
167
|
+
const rootWsgi = files.find((f) => path.dirname(f) === path.resolve(cwd));
|
|
168
|
+
if (rootWsgi) return rootWsgi;
|
|
169
|
+
|
|
170
|
+
return files[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} [cwd]
|
|
175
|
+
* @returns {string} gunicorn target, e.g. `myapp.wsgi:application`
|
|
176
|
+
*/
|
|
177
|
+
export function detectDjangoWsgiTarget(cwd = process.cwd()) {
|
|
178
|
+
const wsgiAbs = pickPreferredWsgiFile(cwd);
|
|
179
|
+
if (!wsgiAbs) return 'config.wsgi:application';
|
|
180
|
+
return wsgiPathToGunicornTarget(cwd, wsgiAbs) || 'config.wsgi:application';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {string} filePath
|
|
185
|
+
* @returns {'app'|'application'|null}
|
|
186
|
+
*/
|
|
187
|
+
function detectFlaskCallableName(filePath) {
|
|
188
|
+
let content;
|
|
189
|
+
try {
|
|
190
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// Prefer explicit assignment / factory patterns for `app` then `application`
|
|
195
|
+
if (
|
|
196
|
+
/\bapp\s*=\s*/.test(content) ||
|
|
197
|
+
/\bcreate_app\s*\(/.test(content) ||
|
|
198
|
+
/\bFlask\s*\(/.test(content)
|
|
199
|
+
) {
|
|
200
|
+
// If both exist, prefer `application` only when `app` is absent as assignment
|
|
201
|
+
if (/\bapp\s*=\s*/.test(content) || /\bFlask\s*\(/.test(content)) {
|
|
202
|
+
if (/\bapplication\s*=\s*/.test(content) && !/\bapp\s*=\s*/.test(content)) {
|
|
203
|
+
return 'application';
|
|
204
|
+
}
|
|
205
|
+
return 'app';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (/\bapplication\s*=\s*/.test(content)) return 'application';
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} [cwd]
|
|
214
|
+
* @returns {string} gunicorn target, e.g. `app:app`
|
|
215
|
+
*/
|
|
216
|
+
export function detectFlaskAppTarget(cwd = process.cwd()) {
|
|
217
|
+
const candidates = ['app.py', 'wsgi.py', 'application.py'];
|
|
218
|
+
for (const name of candidates) {
|
|
219
|
+
const full = path.join(cwd, name);
|
|
220
|
+
if (!fs.existsSync(full)) continue;
|
|
221
|
+
const callable = detectFlaskCallableName(full);
|
|
222
|
+
if (callable) {
|
|
223
|
+
const mod = name.replace(/\.py$/i, '');
|
|
224
|
+
return `${mod}:${callable}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Module package app/__init__.py or app/app.py
|
|
228
|
+
const pkgInit = path.join(cwd, 'app', '__init__.py');
|
|
229
|
+
if (fs.existsSync(pkgInit)) {
|
|
230
|
+
const callable = detectFlaskCallableName(pkgInit);
|
|
231
|
+
if (callable) return `app:${callable}`;
|
|
232
|
+
}
|
|
233
|
+
const pkgApp = path.join(cwd, 'app', 'app.py');
|
|
234
|
+
if (fs.existsSync(pkgApp)) {
|
|
235
|
+
const callable = detectFlaskCallableName(pkgApp);
|
|
236
|
+
if (callable) return `app.app:${callable}`;
|
|
237
|
+
}
|
|
238
|
+
return 'app:app';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default {
|
|
242
|
+
extractGunicornTarget,
|
|
243
|
+
detectDjangoWsgiTarget,
|
|
244
|
+
detectFlaskAppTarget,
|
|
245
|
+
detectDjangoWsgiPackageDir,
|
|
246
|
+
wsgiPathToGunicornTarget,
|
|
247
|
+
};
|