badmfck-api-server 4.1.47 → 4.1.51
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.
|
@@ -10,21 +10,101 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const __1 = require("../..");
|
|
11
11
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
12
|
const undici_1 = require("undici");
|
|
13
|
+
const promises_1 = require("readline/promises");
|
|
14
|
+
const process_1 = require("process");
|
|
15
|
+
function validateConfig(cfg) {
|
|
16
|
+
if (!cfg || typeof cfg !== "object")
|
|
17
|
+
return "not an object";
|
|
18
|
+
if (!cfg.name || !cfg.token || !cfg.host || !cfg.username || !cfg.password)
|
|
19
|
+
return "missing one of: name, token, host, username, password";
|
|
20
|
+
if (cfg.includes && !Array.isArray(cfg.includes))
|
|
21
|
+
return "includes must be an array";
|
|
22
|
+
if (cfg.excludes && !Array.isArray(cfg.excludes))
|
|
23
|
+
return "excludes must be an array";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
async function selectConfig(configs) {
|
|
27
|
+
if (configs.length === 0) {
|
|
28
|
+
throw new Error("No valid deploy configs found");
|
|
29
|
+
}
|
|
30
|
+
if (configs.length === 1) {
|
|
31
|
+
console.log(`Using deploy config: ${configs[0].name}`);
|
|
32
|
+
return configs[0];
|
|
33
|
+
}
|
|
34
|
+
if (!process_1.stdin.isTTY) {
|
|
35
|
+
throw new Error(`Found ${configs.length} deploy configs (${configs.map(c => c.name).join(", ")}) but stdin is not a TTY — ` +
|
|
36
|
+
`cannot prompt for a choice. Pass a specific config explicitly, e.g. Deploy("deploy/<name>.json").`);
|
|
37
|
+
}
|
|
38
|
+
console.log("\nAvailable deploy configs:\n");
|
|
39
|
+
configs.forEach((config, index) => {
|
|
40
|
+
console.log(`[${index + 1}] ${config.name ?? "bad-config-file"}`);
|
|
41
|
+
});
|
|
42
|
+
const rl = (0, promises_1.createInterface)({
|
|
43
|
+
input: process_1.stdin,
|
|
44
|
+
output: process_1.stdout,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
while (true) {
|
|
48
|
+
const answer = await rl.question(`\nChoose config [1-${configs.length}]: `);
|
|
49
|
+
const selectedIndex = Number.parseInt(answer.trim(), 10) - 1;
|
|
50
|
+
if (Number.isInteger(selectedIndex) &&
|
|
51
|
+
selectedIndex >= 0 &&
|
|
52
|
+
selectedIndex < configs.length) {
|
|
53
|
+
return configs[selectedIndex];
|
|
54
|
+
}
|
|
55
|
+
console.error(`Invalid selection. Enter a number from 1 to ${configs.length}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
rl.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
13
62
|
async function Deploy(opt) {
|
|
63
|
+
if (!opt) {
|
|
64
|
+
const dir = path_1.default.resolve("deploy");
|
|
65
|
+
const single = path_1.default.resolve("deploy.json");
|
|
66
|
+
if (fs_1.default.existsSync(dir) && fs_1.default.statSync(dir).isDirectory()) {
|
|
67
|
+
const files = fs_1.default.readdirSync(dir).filter(f => f.endsWith(".json"));
|
|
68
|
+
if (files.length === 0)
|
|
69
|
+
throw new Error(`No .json configs found in ${dir}`);
|
|
70
|
+
const configs = [];
|
|
71
|
+
for (const f of files) {
|
|
72
|
+
const filePath = path_1.default.resolve(dir, f);
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(fs_1.default.readFileSync(filePath).toString("utf-8"));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error(`Skipping ${f}: failed to parse JSON:`, e.message);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const reason = validateConfig(parsed);
|
|
82
|
+
if (reason) {
|
|
83
|
+
console.error(`Skipping ${f}: ${reason}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
configs.push(parsed);
|
|
87
|
+
}
|
|
88
|
+
if (configs.length === 0)
|
|
89
|
+
throw new Error(`No valid deploy configs in ${dir} (all ${files.length} were skipped)`);
|
|
90
|
+
opt = await selectConfig(configs);
|
|
91
|
+
}
|
|
92
|
+
else if (fs_1.default.existsSync(single)) {
|
|
93
|
+
opt = single;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw new Error(`No deploy config found: neither ${dir}/ nor ${single} exists`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
14
99
|
if (typeof opt === "string") {
|
|
15
100
|
if (!fs_1.default.existsSync(opt)) {
|
|
16
101
|
throw new Error(`File not found: ${opt}`);
|
|
17
102
|
}
|
|
18
103
|
opt = JSON.parse(fs_1.default.readFileSync(opt).toString("utf-8"));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error(`Invalid deploy config file: includes must be an array`);
|
|
24
|
-
}
|
|
25
|
-
if (opt.excludes && !Array.isArray(opt.excludes)) {
|
|
26
|
-
throw new Error(`Invalid deploy config file: excludes must be an array`);
|
|
27
|
-
}
|
|
104
|
+
}
|
|
105
|
+
const invalidReason = validateConfig(opt);
|
|
106
|
+
if (invalidReason) {
|
|
107
|
+
throw new Error(`Invalid deploy config: ${invalidReason}`);
|
|
28
108
|
}
|
|
29
109
|
const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
|
|
30
110
|
console.log("Changing Config to live");
|
|
@@ -173,35 +253,12 @@ async function Deploy(opt) {
|
|
|
173
253
|
headersTimeout: 5 * 60 * 1000,
|
|
174
254
|
bodyTimeout: 5 * 60 * 1000,
|
|
175
255
|
});
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
headers: { authorization: `Bearer ${authToken}` },
|
|
183
|
-
body: formData,
|
|
184
|
-
signal: abortController.signal,
|
|
185
|
-
...{ dispatcher: uploadAgent },
|
|
186
|
-
});
|
|
187
|
-
const rawText = await res.text();
|
|
188
|
-
let parsed;
|
|
189
|
-
try {
|
|
190
|
-
parsed = rawText.length > 0 ? JSON.parse(rawText) : null;
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
parsed = rawText;
|
|
194
|
-
}
|
|
195
|
-
uploadResponse = res.ok
|
|
196
|
-
? { ok: true, status: res.status, data: parsed }
|
|
197
|
-
: { ok: false, status: res.status, error: parsed };
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
uploadResponse = { ok: false, error: err };
|
|
201
|
-
}
|
|
202
|
-
finally {
|
|
203
|
-
clearTimeout(abortTimer);
|
|
204
|
-
}
|
|
256
|
+
const uploadResponse = await __1.Http.post(opt.host, formData, {
|
|
257
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
258
|
+
dispatcher: uploadAgent,
|
|
259
|
+
timeoutMs: 5 * 60 * 1000,
|
|
260
|
+
retry: { enabled: false },
|
|
261
|
+
});
|
|
205
262
|
const elapsedMs = Date.now() - startedAt;
|
|
206
263
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
207
264
|
const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
|
|
@@ -281,18 +338,23 @@ async function Deploy(opt) {
|
|
|
281
338
|
console.log(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
282
339
|
console.log(` ${dim("time: ")} ${stamp}`);
|
|
283
340
|
const info = body?.data?.data;
|
|
284
|
-
if (info && typeof info === "object" &&
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
341
|
+
if (info && typeof info === "object" && "ok" in info) {
|
|
342
|
+
if (info.project && info.project !== opt.name) {
|
|
343
|
+
console.log(` ${dim("server project:")} ${info.project}`);
|
|
344
|
+
}
|
|
345
|
+
if (info.bluegreen === true) {
|
|
346
|
+
const slotBadge = info.activeThisDeploy
|
|
347
|
+
? green("← active in production")
|
|
348
|
+
: yellow("(inactive — deploy landed but nginx still points at " + info.active + "; run /pckg/switch to promote)");
|
|
349
|
+
console.log(` ${dim("mode: ")} blue-green`);
|
|
350
|
+
console.log(` ${dim("slot: ")} ${bold(String(info.slot))} ${slotBadge}`);
|
|
351
|
+
if (info.active && info.active !== info.slot) {
|
|
352
|
+
console.log(` ${dim("active: ")} ${info.active}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (info.bluegreen === false) {
|
|
356
|
+
console.log(` ${dim("mode: ")} single-instance ${green("← active")}`);
|
|
292
357
|
}
|
|
293
|
-
}
|
|
294
|
-
else if (info && typeof info === "object" && info.bluegreen === false) {
|
|
295
|
-
console.log(` ${dim("mode: ")} single-instance`);
|
|
296
358
|
}
|
|
297
359
|
if (!emptyResponse) {
|
|
298
360
|
const payload = JSON.stringify(body);
|
|
@@ -119,6 +119,7 @@ export declare class DeployerService extends BaseService {
|
|
|
119
119
|
}>;
|
|
120
120
|
tokensMatch(a: string | undefined, b: string | undefined): boolean;
|
|
121
121
|
private _checkUserAuth;
|
|
122
|
+
private static _extractPort;
|
|
122
123
|
private _renderNginx;
|
|
123
124
|
runPM2(found: IConfig, projectState?: IProjectState | null): Promise<IError | null>;
|
|
124
125
|
private static readonly SKIP_DIRS;
|
|
@@ -246,7 +246,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
246
246
|
}
|
|
247
247
|
if (found.nginx?.config_dir && (!projectState || projectStateWasCreated)) {
|
|
248
248
|
const activeCfg = (found.config ?? {});
|
|
249
|
-
const nginxErr = await this._renderNginx(found, data.name, activeCfg
|
|
249
|
+
const nginxErr = await this._renderNginx(found, data.name, DeployerService._extractPort(activeCfg));
|
|
250
250
|
if (nginxErr) {
|
|
251
251
|
if (found.email && found.email.length > 0) {
|
|
252
252
|
for (let email of found.email) {
|
|
@@ -366,7 +366,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
366
366
|
if (found.nginx?.config_dir) {
|
|
367
367
|
const slotCfg = target === "blue" ? found.bluegreen.blue_config : found.bluegreen.green_config;
|
|
368
368
|
const merged = { ...(found.config ?? {}), ...(slotCfg ?? {}) };
|
|
369
|
-
const nginxErr = await this._renderNginx(found, data.name, merged
|
|
369
|
+
const nginxErr = await this._renderNginx(found, data.name, DeployerService._extractPort(merged));
|
|
370
370
|
if (nginxErr) {
|
|
371
371
|
(0, LogService_1.logError)("nginx render during switch failed (state already flipped): " + nginxErr.message);
|
|
372
372
|
hookError = nginxErr.message;
|
|
@@ -441,11 +441,23 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
441
441
|
}
|
|
442
442
|
return matched;
|
|
443
443
|
}
|
|
444
|
+
static _extractPort(config) {
|
|
445
|
+
if (!config || typeof config !== "object")
|
|
446
|
+
return "";
|
|
447
|
+
for (const key of Object.keys(config)) {
|
|
448
|
+
if (key.toLowerCase() === "port") {
|
|
449
|
+
const v = config[key];
|
|
450
|
+
if (v !== null && v !== undefined && String(v).length > 0)
|
|
451
|
+
return String(v);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return "";
|
|
455
|
+
}
|
|
444
456
|
async _renderNginx(found, projectName, activePort) {
|
|
445
457
|
if (!found.nginx?.config_dir)
|
|
446
458
|
return null;
|
|
447
459
|
if (!activePort) {
|
|
448
|
-
return { code: 52, message: "Cannot render nginx: no
|
|
460
|
+
return { code: 52, message: "Cannot render nginx: no port found in config for project " + projectName + " (expected a 'port' key in config or blue_config/green_config)", httpStatus: 500 };
|
|
449
461
|
}
|
|
450
462
|
const dir = found.nginx.config_dir;
|
|
451
463
|
try {
|
|
@@ -43,6 +43,32 @@ function normalizeHeaders(headers) {
|
|
|
43
43
|
function isRetryableStatus(status, retryOnStatuses) {
|
|
44
44
|
return retryOnStatuses.includes(status);
|
|
45
45
|
}
|
|
46
|
+
function isFormDataLike(value) {
|
|
47
|
+
return (value !== null &&
|
|
48
|
+
typeof value === "object" &&
|
|
49
|
+
Object.prototype.toString.call(value) === "[object FormData]" &&
|
|
50
|
+
typeof value.entries === "function");
|
|
51
|
+
}
|
|
52
|
+
function normalizeFormData(source) {
|
|
53
|
+
if (source instanceof undici_1.FormData)
|
|
54
|
+
return source;
|
|
55
|
+
const target = new undici_1.FormData();
|
|
56
|
+
for (const [name, value] of source.entries()) {
|
|
57
|
+
if (typeof value === "string") {
|
|
58
|
+
target.append(name, value);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
target.append(name, value, value.name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return target;
|
|
65
|
+
}
|
|
66
|
+
function removeContentTypeHeader(headers) {
|
|
67
|
+
for (const key of Object.keys(headers)) {
|
|
68
|
+
if (key.toLowerCase() === "content-type")
|
|
69
|
+
delete headers[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
46
72
|
function isLikelyNetworkError(err) {
|
|
47
73
|
if (!err || typeof err !== "object")
|
|
48
74
|
return false;
|
|
@@ -423,11 +449,12 @@ class Http {
|
|
|
423
449
|
let body = undefined;
|
|
424
450
|
const hasBody = opts.body !== undefined && opts.body !== null;
|
|
425
451
|
const allowBody = opts.allowBody ?? (opts.method !== "GET" && opts.method !== "DELETE");
|
|
452
|
+
let isMultipart = false;
|
|
426
453
|
if (hasBody && allowBody) {
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
if (isFormDataLike(opts.body)) {
|
|
455
|
+
isMultipart = true;
|
|
456
|
+
body = normalizeFormData(opts.body);
|
|
457
|
+
removeContentTypeHeader(headers);
|
|
431
458
|
}
|
|
432
459
|
else {
|
|
433
460
|
const ct = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
|
|
@@ -442,6 +469,9 @@ class Http {
|
|
|
442
469
|
}
|
|
443
470
|
}
|
|
444
471
|
}
|
|
472
|
+
if (isMultipart && retryCfg.maxAttempts !== 1) {
|
|
473
|
+
retryCfg.maxAttempts = 1;
|
|
474
|
+
}
|
|
445
475
|
const responseType = opts.responseType ?? "json";
|
|
446
476
|
const throwOnJsonParseError = opts.throwOnJsonParseError ?? false;
|
|
447
477
|
let attemptsMade = 0;
|