@nmakarov/cli-toolkit 0.51.0 → 0.53.0
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/dist/cli-runner.cjs +21 -12
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +21 -12
- package/dist/cli-runner.js.map +1 -1
- package/dist/deploy.cjs +192 -16
- package/dist/deploy.cjs.map +1 -1
- package/dist/deploy.js +181 -10
- package/dist/deploy.js.map +1 -1
- package/dist/index.cjs +204 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +191 -18
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +12 -5
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +12 -5
- package/dist/init.js.map +1 -1
- package/dist/tasks.cjs +10 -8
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +10 -8
- package/dist/tasks.js.map +1 -1
- package/package.json +1 -1
- package/scripts/deploy/cli.js +5 -1
package/dist/deploy.js
CHANGED
|
@@ -11,8 +11,13 @@ function defineService(service) {
|
|
|
11
11
|
const pm2 = {
|
|
12
12
|
appName: service.name,
|
|
13
13
|
args: "",
|
|
14
|
+
stopAllowance: 60,
|
|
14
15
|
...service.pm2
|
|
15
16
|
};
|
|
17
|
+
if (pm2.killTimeout == null) {
|
|
18
|
+
const stopSec = Number(pm2.stopAllowance);
|
|
19
|
+
pm2.killTimeout = Number.isFinite(stopSec) && stopSec > 0 ? Math.floor(stopSec * 1e3) + 5e3 : 65e3;
|
|
20
|
+
}
|
|
16
21
|
const nginx = service.nginx ? { siteName: service.name, ...service.nginx } : null;
|
|
17
22
|
return {
|
|
18
23
|
repoDirName: deriveRepoDirName(service.repoUrl),
|
|
@@ -93,6 +98,30 @@ function run(cmd, args, options = {}) {
|
|
|
93
98
|
function runShell(command, options = {}) {
|
|
94
99
|
return run("bash", ["-lc", command], options);
|
|
95
100
|
}
|
|
101
|
+
function runCapture(cmd, args, options = {}) {
|
|
102
|
+
const { cwd, env, logger, allowFail = false } = options;
|
|
103
|
+
return new Promise((resolve2, reject) => {
|
|
104
|
+
logger?.info?.(`$ ${cmd} ${args.join(" ")}${cwd ? ` (cwd=${cwd})` : ""}`);
|
|
105
|
+
const child = spawn(cmd, args, {
|
|
106
|
+
cwd,
|
|
107
|
+
env: env ?? process.env,
|
|
108
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
109
|
+
});
|
|
110
|
+
let stdout = "";
|
|
111
|
+
let stderr = "";
|
|
112
|
+
child.stdout?.on("data", (chunk) => {
|
|
113
|
+
stdout += chunk.toString();
|
|
114
|
+
});
|
|
115
|
+
child.stderr?.on("data", (chunk) => {
|
|
116
|
+
stderr += chunk.toString();
|
|
117
|
+
});
|
|
118
|
+
child.on("error", reject);
|
|
119
|
+
child.on("close", (code) => {
|
|
120
|
+
if (code === 0 || allowFail) resolve2({ stdout, stderr, code });
|
|
121
|
+
else reject(new Error(`${cmd} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
96
125
|
|
|
97
126
|
// src/deploy/log.js
|
|
98
127
|
import { appendFile, mkdir } from "fs/promises";
|
|
@@ -310,7 +339,7 @@ async function runReleaseTests(service, releasePath, paths, options = {}) {
|
|
|
310
339
|
// src/deploy/prune.js
|
|
311
340
|
import { rm as rm2, readlink as readlink2 } from "fs/promises";
|
|
312
341
|
async function pruneReleases(service, paths, options = {}) {
|
|
313
|
-
const { dryRun = false, logger = console } = options;
|
|
342
|
+
const { dryRun = false, logger = console, protect = [] } = options;
|
|
314
343
|
const keep = service.keepReleases ?? 3;
|
|
315
344
|
const releases = await listReleases(paths);
|
|
316
345
|
let activeName = null;
|
|
@@ -324,6 +353,9 @@ async function pruneReleases(service, paths, options = {}) {
|
|
|
324
353
|
if (keepSet.size < keep) keepSet.add(rel.name);
|
|
325
354
|
}
|
|
326
355
|
if (activeName) keepSet.add(activeName);
|
|
356
|
+
for (const name of protect) {
|
|
357
|
+
if (name) keepSet.add(name);
|
|
358
|
+
}
|
|
327
359
|
const toRemove = releases.filter((rel) => !keepSet.has(rel.name));
|
|
328
360
|
for (const rel of toRemove) {
|
|
329
361
|
if (dryRun) {
|
|
@@ -337,14 +369,101 @@ async function pruneReleases(service, paths, options = {}) {
|
|
|
337
369
|
}
|
|
338
370
|
|
|
339
371
|
// src/deploy/pm2.js
|
|
372
|
+
function sleep(ms) {
|
|
373
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
374
|
+
}
|
|
375
|
+
async function getPm2Process(appName, options = {}) {
|
|
376
|
+
const { logger } = options;
|
|
377
|
+
const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
|
|
378
|
+
let list;
|
|
379
|
+
try {
|
|
380
|
+
list = JSON.parse(stdout || "[]");
|
|
381
|
+
} catch (err) {
|
|
382
|
+
throw new Error(`pm2 jlist: invalid JSON (${err?.message ?? err})`);
|
|
383
|
+
}
|
|
384
|
+
if (!Array.isArray(list)) return null;
|
|
385
|
+
return list.find((p) => p?.name === appName) ?? null;
|
|
386
|
+
}
|
|
387
|
+
async function waitPm2(appName, predicate, options = {}) {
|
|
388
|
+
const {
|
|
389
|
+
timeoutMs = 65e3,
|
|
390
|
+
pollMs = 500,
|
|
391
|
+
dryRun = false,
|
|
392
|
+
logger = console,
|
|
393
|
+
label = "condition"
|
|
394
|
+
} = options;
|
|
395
|
+
if (dryRun) {
|
|
396
|
+
logger.info?.(`[dryRun] would wait pm2 ${appName} for ${label}`);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const deadline = Date.now() + timeoutMs;
|
|
400
|
+
let lastStatus = "(unknown)";
|
|
401
|
+
while (Date.now() < deadline) {
|
|
402
|
+
const proc = await getPm2Process(appName, { logger });
|
|
403
|
+
lastStatus = proc?.pm2_env?.status ?? "(missing)";
|
|
404
|
+
if (predicate(proc)) return proc;
|
|
405
|
+
await sleep(pollMs);
|
|
406
|
+
}
|
|
407
|
+
throw new Error(
|
|
408
|
+
`pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
340
411
|
async function reloadPm2(paths, options = {}) {
|
|
341
|
-
const { dryRun = false, logger = console } = options;
|
|
412
|
+
const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
|
|
342
413
|
if (dryRun) {
|
|
343
414
|
logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
|
|
344
415
|
return;
|
|
345
416
|
}
|
|
346
417
|
await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
|
|
347
418
|
logger.info("pm2 reloaded");
|
|
419
|
+
if (appName) {
|
|
420
|
+
await waitPm2(
|
|
421
|
+
appName,
|
|
422
|
+
(proc) => proc?.pm2_env?.status === "online",
|
|
423
|
+
{ timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
|
|
424
|
+
);
|
|
425
|
+
logger.info(`pm2 ${appName} online`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function stopPm2(appName, options = {}) {
|
|
429
|
+
const { dryRun = false, logger = console, waitTimeoutMs = 65e3 } = options;
|
|
430
|
+
if (dryRun) {
|
|
431
|
+
logger.info(`[dryRun] would pm2 stop ${appName}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const before = await getPm2Process(appName, { logger });
|
|
435
|
+
if (!before) {
|
|
436
|
+
logger.info(`pm2 ${appName}: not present (already stopped)`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (before.pm2_env?.status === "stopped") {
|
|
440
|
+
logger.info(`pm2 ${appName}: already stopped`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await runShell(`pm2 stop "${appName}"`, { logger });
|
|
444
|
+
await waitPm2(
|
|
445
|
+
appName,
|
|
446
|
+
(proc) => !proc || proc.pm2_env?.status === "stopped",
|
|
447
|
+
{ timeoutMs: waitTimeoutMs, logger, label: "stopped" }
|
|
448
|
+
);
|
|
449
|
+
logger.info(`pm2 ${appName} stopped`);
|
|
450
|
+
}
|
|
451
|
+
async function startPm2(paths, options = {}) {
|
|
452
|
+
const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
|
|
453
|
+
if (dryRun) {
|
|
454
|
+
logger.info(`[dryRun] would pm2 start ${paths.ecosystem} --update-env`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
|
|
458
|
+
logger.info("pm2 started");
|
|
459
|
+
if (appName) {
|
|
460
|
+
await waitPm2(
|
|
461
|
+
appName,
|
|
462
|
+
(proc) => proc?.pm2_env?.status === "online",
|
|
463
|
+
{ timeoutMs: waitTimeoutMs, logger, label: "online after start" }
|
|
464
|
+
);
|
|
465
|
+
logger.info(`pm2 ${appName} online`);
|
|
466
|
+
}
|
|
348
467
|
}
|
|
349
468
|
|
|
350
469
|
// src/deploy/nginx.js
|
|
@@ -573,10 +692,26 @@ async function requireDeployRoot(parentDir, serviceRoot) {
|
|
|
573
692
|
This is meant to run on the target host (where ${parentDir} exists). For a local dry run use --appsRoot=/tmp/<service>.`
|
|
574
693
|
);
|
|
575
694
|
}
|
|
695
|
+
function resolveKillTimeoutMs(pm2) {
|
|
696
|
+
const explicit = Number(pm2?.killTimeout);
|
|
697
|
+
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
698
|
+
const stopSec = Number(pm2?.stopAllowance);
|
|
699
|
+
if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
|
|
700
|
+
return 65e3;
|
|
701
|
+
}
|
|
702
|
+
function resolvePm2Args(pm2) {
|
|
703
|
+
const base = String(pm2?.args ?? "").trim();
|
|
704
|
+
const stopSec = Number(pm2?.stopAllowance);
|
|
705
|
+
if (!Number.isFinite(stopSec) || stopSec <= 0) return base;
|
|
706
|
+
if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
|
|
707
|
+
return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
|
|
708
|
+
}
|
|
576
709
|
function buildEcosystemConfig(service, paths) {
|
|
577
710
|
const { pm2 } = service;
|
|
578
711
|
const outLog = join8(paths.logs, `${pm2.appName}.out.log`);
|
|
579
712
|
const errLog = join8(paths.logs, `${pm2.appName}.err.log`);
|
|
713
|
+
const killTimeoutMs = resolveKillTimeoutMs(pm2);
|
|
714
|
+
const args = resolvePm2Args(pm2);
|
|
580
715
|
return `/**
|
|
581
716
|
* pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
|
|
582
717
|
* service manifest (init/deploy). Do not hand-edit; change pm2.* in the
|
|
@@ -588,7 +723,7 @@ module.exports = {
|
|
|
588
723
|
name: "${pm2.appName}",
|
|
589
724
|
script: "${pm2.script}",
|
|
590
725
|
cwd: "${paths.current}",
|
|
591
|
-
args:
|
|
726
|
+
args: ${JSON.stringify(args)},
|
|
592
727
|
instances: 1,
|
|
593
728
|
exec_mode: "fork",
|
|
594
729
|
autorestart: true,
|
|
@@ -596,6 +731,8 @@ module.exports = {
|
|
|
596
731
|
max_restarts: 10,
|
|
597
732
|
restart_delay: 2000,
|
|
598
733
|
max_memory_restart: "1500M",
|
|
734
|
+
// Grace window after SIGINT/SIGTERM before SIGKILL (ms). Align with --stopAllowance.
|
|
735
|
+
kill_timeout: ${killTimeoutMs},
|
|
599
736
|
out_file: "${outLog}",
|
|
600
737
|
error_file: "${errLog}",
|
|
601
738
|
merge_logs: true,
|
|
@@ -796,15 +933,27 @@ async function bootstrapHost(service, options = {}) {
|
|
|
796
933
|
}
|
|
797
934
|
|
|
798
935
|
// src/deploy/deploy-service.js
|
|
936
|
+
import { readlink as readlink3 } from "fs/promises";
|
|
937
|
+
function resolveKillTimeoutMs2(service) {
|
|
938
|
+
const explicit = Number(service.pm2?.killTimeout);
|
|
939
|
+
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
940
|
+
const stopSec = Number(service.pm2?.stopAllowance);
|
|
941
|
+
if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
|
|
942
|
+
return 65e3;
|
|
943
|
+
}
|
|
799
944
|
async function deployService(service, options = {}) {
|
|
800
945
|
const {
|
|
801
946
|
dryRun = false,
|
|
802
947
|
skipPull = false,
|
|
803
948
|
skipTests = false,
|
|
804
949
|
skipNginx = false,
|
|
950
|
+
stopFirst = false,
|
|
805
951
|
logger = console
|
|
806
952
|
} = options;
|
|
807
953
|
const paths = servicePaths(service);
|
|
954
|
+
const appName = service.pm2.appName;
|
|
955
|
+
const killTimeoutMs = resolveKillTimeoutMs2(service);
|
|
956
|
+
const waitTimeoutMs = killTimeoutMs + 1e4;
|
|
808
957
|
await initServiceStructure(service, { dryRun, logger });
|
|
809
958
|
if (!skipPull) await pullRepo(service, { dryRun, logger });
|
|
810
959
|
await syncEnv(service, { dryRun, logger });
|
|
@@ -812,14 +961,31 @@ async function deployService(service, options = {}) {
|
|
|
812
961
|
await writeReleaseBuildInfo(service, releasePath, { stamp, dryRun, logger });
|
|
813
962
|
await installDeps(service, releasePath, paths, { dryRun, logger });
|
|
814
963
|
if (!skipTests) await runReleaseTests(service, releasePath, paths, { dryRun, logger });
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
964
|
+
let previousReleaseName = null;
|
|
965
|
+
try {
|
|
966
|
+
const prev = await readlink3(paths.current);
|
|
967
|
+
previousReleaseName = prev.split("/").pop() || null;
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
if (stopFirst) {
|
|
971
|
+
logger.info(`deploy mode=stopFirst app=${appName} killTimeoutMs=${killTimeoutMs}`);
|
|
972
|
+
await stopPm2(appName, { dryRun, logger, waitTimeoutMs });
|
|
973
|
+
await activateRelease(releasePath, paths, { dryRun, logger });
|
|
974
|
+
await pruneReleases(service, paths, { dryRun, logger });
|
|
975
|
+
await startPm2(paths, { dryRun, logger, appName, waitTimeoutMs });
|
|
976
|
+
} else {
|
|
977
|
+
logger.info(
|
|
978
|
+
`deploy mode=rolling app=${appName} killTimeoutMs=${killTimeoutMs}` + (previousReleaseName ? ` previous=${previousReleaseName}` : "")
|
|
979
|
+
);
|
|
980
|
+
await activateRelease(releasePath, paths, { dryRun, logger });
|
|
981
|
+
await reloadPm2(paths, { dryRun, logger, appName, waitTimeoutMs });
|
|
982
|
+
await pruneReleases(service, paths, { dryRun, logger });
|
|
983
|
+
}
|
|
818
984
|
if (!skipNginx) await enableNginxUpstream(service, { dryRun, logger });
|
|
819
|
-
const summary = `deploy complete stamp=${stamp} dryRun=${dryRun}`;
|
|
985
|
+
const summary = `deploy complete stamp=${stamp} mode=${stopFirst ? "stopFirst" : "rolling"} dryRun=${dryRun}`;
|
|
820
986
|
logger.info(summary);
|
|
821
987
|
if (!dryRun) await appendDeployLog(paths.deployLog, summary);
|
|
822
|
-
return { stamp, releasePath };
|
|
988
|
+
return { stamp, releasePath, mode: stopFirst ? "stopFirst" : "rolling" };
|
|
823
989
|
}
|
|
824
990
|
|
|
825
991
|
// src/deploy/provision-service.js
|
|
@@ -860,7 +1026,7 @@ async function provisionService(service, options = {}) {
|
|
|
860
1026
|
}
|
|
861
1027
|
|
|
862
1028
|
// src/deploy/rollback-service.js
|
|
863
|
-
import { readlink as
|
|
1029
|
+
import { readlink as readlink4 } from "fs/promises";
|
|
864
1030
|
async function rollbackService(service, options = {}) {
|
|
865
1031
|
const { release: targetName, dryRun = false, logger = console } = options;
|
|
866
1032
|
const paths = servicePaths(service);
|
|
@@ -868,7 +1034,7 @@ async function rollbackService(service, options = {}) {
|
|
|
868
1034
|
if (releases.length === 0) throw new Error("No releases to roll back to");
|
|
869
1035
|
let activeName = null;
|
|
870
1036
|
try {
|
|
871
|
-
const target = await
|
|
1037
|
+
const target = await readlink4(paths.current);
|
|
872
1038
|
activeName = target.split("/").pop();
|
|
873
1039
|
} catch {
|
|
874
1040
|
throw new Error("No active release (current symlink missing)");
|
|
@@ -1106,6 +1272,7 @@ export {
|
|
|
1106
1272
|
ensureEnvOnRemote,
|
|
1107
1273
|
ensureRemoteRepo,
|
|
1108
1274
|
ensureRepoDependencies,
|
|
1275
|
+
getPm2Process,
|
|
1109
1276
|
initServiceStructure,
|
|
1110
1277
|
installDeps,
|
|
1111
1278
|
installOperatorShell,
|
|
@@ -1127,6 +1294,7 @@ export {
|
|
|
1127
1294
|
resolveServiceFrom,
|
|
1128
1295
|
rollbackService,
|
|
1129
1296
|
run,
|
|
1297
|
+
runCapture,
|
|
1130
1298
|
runReleaseTests,
|
|
1131
1299
|
runRemoteCli,
|
|
1132
1300
|
runRemoteStatus,
|
|
@@ -1135,7 +1303,10 @@ export {
|
|
|
1135
1303
|
servicePaths,
|
|
1136
1304
|
shellQuote,
|
|
1137
1305
|
sshRun,
|
|
1306
|
+
startPm2,
|
|
1307
|
+
stopPm2,
|
|
1138
1308
|
syncEnv,
|
|
1309
|
+
waitPm2,
|
|
1139
1310
|
writeReleaseBuildInfo
|
|
1140
1311
|
};
|
|
1141
1312
|
//# sourceMappingURL=deploy.js.map
|