@nmakarov/cli-toolkit 0.53.0 → 0.57.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/deploy.js CHANGED
@@ -54,6 +54,8 @@ function servicePaths(service) {
54
54
  current: join(root, "current"),
55
55
  sharedEnv: join(root, "shared", ".env"),
56
56
  ecosystem: join(root, "shared", "ecosystem.config.cjs"),
57
+ /** Preloaded by pm2 `node_args --require` so ENV survives flaky restarts. */
58
+ ensureEnv: join(root, "shared", "ensure-env.cjs"),
57
59
  deployLog: join(root, "logs", "deploy.log"),
58
60
  lockHashFile: join(root, "shared", ".package-lock.sha256")
59
61
  };
@@ -372,6 +374,16 @@ async function pruneReleases(service, paths, options = {}) {
372
374
  function sleep(ms) {
373
375
  return new Promise((resolve2) => setTimeout(resolve2, ms));
374
376
  }
377
+ function isPidAlive(pid) {
378
+ const n = Number(pid);
379
+ if (!Number.isFinite(n) || n <= 0) return false;
380
+ try {
381
+ process.kill(n, 0);
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
375
387
  async function getPm2Process(appName, options = {}) {
376
388
  const { logger } = options;
377
389
  const { stdout } = await runCapture("bash", ["-lc", "pm2 jlist"], { logger });
@@ -408,21 +420,44 @@ async function waitPm2(appName, predicate, options = {}) {
408
420
  `pm2 wait timed out after ${timeoutMs}ms for ${appName} (${label}); last status=${lastStatus}`
409
421
  );
410
422
  }
423
+ function isFreshOnline(proc, oldPid) {
424
+ if (proc?.pm2_env?.status !== "online") return false;
425
+ const pid = Number(proc.pid);
426
+ if (!Number.isFinite(pid) || pid <= 0) return false;
427
+ if (oldPid != null) {
428
+ if (pid === oldPid) return false;
429
+ if (isPidAlive(oldPid)) return false;
430
+ }
431
+ return true;
432
+ }
411
433
  async function reloadPm2(paths, options = {}) {
412
434
  const { dryRun = false, logger = console, appName = null, waitTimeoutMs = 65e3 } = options;
413
435
  if (dryRun) {
414
436
  logger.info(`[dryRun] would pm2 startOrReload ${paths.ecosystem} --update-env`);
415
437
  return;
416
438
  }
439
+ let oldPid = null;
440
+ if (appName) {
441
+ const before = await getPm2Process(appName, { logger });
442
+ const n = Number(before?.pid);
443
+ if (Number.isFinite(n) && n > 0 && before?.pm2_env?.status === "online") {
444
+ oldPid = n;
445
+ logger.info(`pm2 ${appName}: reloading (old pid=${oldPid})`);
446
+ }
447
+ }
417
448
  await runShell(`pm2 startOrReload "${paths.ecosystem}" --update-env`, { logger });
418
449
  logger.info("pm2 reloaded");
419
450
  if (appName) {
420
- await waitPm2(
451
+ const proc = await waitPm2(
421
452
  appName,
422
- (proc) => proc?.pm2_env?.status === "online",
423
- { timeoutMs: waitTimeoutMs, logger, label: "online after reload" }
453
+ (p) => isFreshOnline(p, oldPid),
454
+ {
455
+ timeoutMs: waitTimeoutMs,
456
+ logger,
457
+ label: oldPid ? `online on new pid (old ${oldPid} gone)` : "online after reload"
458
+ }
424
459
  );
425
- logger.info(`pm2 ${appName} online`);
460
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
426
461
  }
427
462
  }
428
463
  async function stopPm2(appName, options = {}) {
@@ -440,10 +475,15 @@ async function stopPm2(appName, options = {}) {
440
475
  logger.info(`pm2 ${appName}: already stopped`);
441
476
  return;
442
477
  }
478
+ const oldPid = Number(before.pid);
443
479
  await runShell(`pm2 stop "${appName}"`, { logger });
444
480
  await waitPm2(
445
481
  appName,
446
- (proc) => !proc || proc.pm2_env?.status === "stopped",
482
+ (proc) => {
483
+ if (proc && proc.pm2_env?.status !== "stopped") return false;
484
+ if (Number.isFinite(oldPid) && oldPid > 0 && isPidAlive(oldPid)) return false;
485
+ return true;
486
+ },
447
487
  { timeoutMs: waitTimeoutMs, logger, label: "stopped" }
448
488
  );
449
489
  logger.info(`pm2 ${appName} stopped`);
@@ -457,12 +497,12 @@ async function startPm2(paths, options = {}) {
457
497
  await runShell(`pm2 start "${paths.ecosystem}" --update-env`, { logger });
458
498
  logger.info("pm2 started");
459
499
  if (appName) {
460
- await waitPm2(
500
+ const proc = await waitPm2(
461
501
  appName,
462
- (proc) => proc?.pm2_env?.status === "online",
502
+ (p) => isFreshOnline(p, null),
463
503
  { timeoutMs: waitTimeoutMs, logger, label: "online after start" }
464
504
  );
465
- logger.info(`pm2 ${appName} online`);
505
+ logger.info(`pm2 ${appName} online pid=${proc?.pid ?? "?"}`);
466
506
  }
467
507
  }
468
508
 
@@ -706,12 +746,23 @@ function resolvePm2Args(pm2) {
706
746
  if (/(?:^|\s)--stopAllowance=/.test(base)) return base;
707
747
  return `${base}${base ? " " : ""}--stopAllowance=${Math.floor(stopSec)}`.trim();
708
748
  }
749
+ function buildEnsureEnvScript() {
750
+ return `/**
751
+ * Written by cli-toolkit deploy (init-structure). Do not hand-edit.
752
+ * Preloaded via pm2 node_args --require so Args sees ENV=production even when
753
+ * a post-SIGKILL restart briefly omits ecosystem env (otherwise Db \u2192 local:6032).
754
+ */
755
+ if (!process.env.ENV) process.env.ENV = "production";
756
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
757
+ `;
758
+ }
709
759
  function buildEcosystemConfig(service, paths) {
710
760
  const { pm2 } = service;
711
761
  const outLog = join8(paths.logs, `${pm2.appName}.out.log`);
712
762
  const errLog = join8(paths.logs, `${pm2.appName}.err.log`);
713
763
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
714
764
  const args = resolvePm2Args(pm2);
765
+ const ensureEnv = paths.ensureEnv;
715
766
  return `/**
716
767
  * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
717
768
  * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
@@ -724,6 +775,8 @@ module.exports = {
724
775
  script: "${pm2.script}",
725
776
  cwd: "${paths.current}",
726
777
  args: ${JSON.stringify(args)},
778
+ // Absolute --require so ENV is set before Args constructs (see shared/ensure-env.cjs).
779
+ node_args: ${JSON.stringify(`--require ${ensureEnv}`)},
727
780
  instances: 1,
728
781
  exec_mode: "fork",
729
782
  autorestart: true,
@@ -765,9 +818,17 @@ async function initServiceStructure(service, options = {}) {
765
818
  created.push(dir);
766
819
  }
767
820
  const ecosystemExisted = await pathExists4(paths.ecosystem);
821
+ const ensureEnvExisted = await pathExists4(paths.ensureEnv);
768
822
  if (!dryRun) {
823
+ await writeFile5(paths.ensureEnv, buildEnsureEnvScript(), { mode: 420 });
769
824
  await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
770
825
  }
826
+ if (ensureEnvExisted) {
827
+ logger.info(`synced ${paths.ensureEnv}`);
828
+ } else {
829
+ created.push(paths.ensureEnv);
830
+ logger.info(`seeded ${paths.ensureEnv}`);
831
+ }
771
832
  if (ecosystemExisted) {
772
833
  logger.info(`synced ${paths.ecosystem} from manifest`);
773
834
  } else {
@@ -1276,6 +1337,8 @@ export {
1276
1337
  initServiceStructure,
1277
1338
  installDeps,
1278
1339
  installOperatorShell,
1340
+ isFreshOnline,
1341
+ isPidAlive,
1279
1342
  listReleases,
1280
1343
  loadServices,
1281
1344
  npmEnv,