@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
|
@@ -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
|
+
};
|