underpost 3.2.12 → 3.2.21

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.
@@ -133,10 +133,21 @@ class UnderpostRepository {
133
133
  p: undefined,
134
134
  bc: '',
135
135
  isRemoteRepo: '',
136
+ hasChanges: false,
136
137
  },
137
138
  ) {
138
139
  if (!repoPath) repoPath = '.';
139
140
 
141
+ if (options.hasChanges) {
142
+ const status = shellExec(`cd ${repoPath} && git status --porcelain`, {
143
+ stdout: true,
144
+ silent: true,
145
+ disableLog: true,
146
+ }).trim();
147
+ process.stdout.write(status ? '1' : '');
148
+ return;
149
+ }
150
+
140
151
  if (options.isRemoteRepo) {
141
152
  const accessible = Underpost.repo.isRemoteRepo(options.isRemoteRepo);
142
153
  console.log(accessible);
@@ -556,7 +567,7 @@ class UnderpostRepository {
556
567
  // Handle sync-conf operation
557
568
  if (options.syncConf) {
558
569
  logger.info(`Syncing configuration for deploy ID: ${deployId}`);
559
- shellExec(`node bin/build ${deployId} conf`);
570
+ shellExec(`node bin/build ${deployId} --conf`);
560
571
  logger.info('Configuration synced successfully');
561
572
  return resolve(true);
562
573
  }
@@ -608,7 +619,8 @@ class UnderpostRepository {
608
619
  const npmRoot = getNpmRootPath();
609
620
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
610
621
  const destFolder = `./${projectName}`;
611
- logger.info('build app', { destFolder });
622
+ const deployId = projectName.startsWith('dd-') ? projectName : `dd-${projectName}`;
623
+ logger.info('build app', { destFolder, deployId });
612
624
  if (fs.existsSync(destFolder)) fs.removeSync(destFolder);
613
625
  fs.mkdirSync(destFolder, { recursive: true });
614
626
  if (!options.dev) {
@@ -621,8 +633,9 @@ class UnderpostRepository {
621
633
  UnderpostRepository.API.initLocalRepo({ path: destFolder });
622
634
  shellExec(`cd ${destFolder} && git add . && git commit -m "Base template implementation"`);
623
635
  }
624
- shellExec(`cd ${destFolder} && npm run build`);
625
- shellExec(`cd ${destFolder} && npm run dev`);
636
+ shellExec(`cd ${destFolder} && node bin new --deploy-id ${deployId} --default-conf`);
637
+ shellExec(`cd ${destFolder} && node bin client ${deployId}`);
638
+ shellExec(`cd ${destFolder} && DEPLOY_ID=${deployId} npm run dev`);
626
639
  }
627
640
  return resolve(true);
628
641
  } catch (error) {
@@ -852,6 +865,7 @@ class UnderpostRepository {
852
865
  }
853
866
  }
854
867
  await buildClient({
868
+ deployId: resolvedDeployId,
855
869
  buildZip: options.buildZip || false,
856
870
  split: options.split || '',
857
871
  fullBuild: options.liteBuild ? false : true,
@@ -862,7 +876,12 @@ class UnderpostRepository {
862
876
  logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
863
877
  continue;
864
878
  }
865
- await Underpost.repo.client(replicaDeployId);
879
+ await Underpost.repo.client(replicaDeployId, '', '', '', {
880
+ buildZip: options.buildZip || false,
881
+ split: options.split || '',
882
+ liteBuild: options.liteBuild || false,
883
+ iconsBuild: options.iconsBuild || false,
884
+ });
866
885
  }
867
886
 
868
887
  return resolve(true);
@@ -937,7 +956,7 @@ Prevent build private config repo.`,
937
956
  deployVersion: packageJsonDeploy.version,
938
957
  };
939
958
  }
940
- shellExec(`node bin/build ${deployId} conf`);
959
+ shellExec(`node bin/build ${deployId} --conf`);
941
960
  return {
942
961
  validVersion: true,
943
962
  engineVersion: packageJsonEngine.version,
@@ -1374,8 +1393,9 @@ Prevent build private config repo.`,
1374
1393
  const gitEmail = process.env.GITHUB_EMAIL || `development@underpost.net`;
1375
1394
 
1376
1395
  if (!fs.existsSync(`${repoPath}/.git`)) {
1377
- shellExec(`cd "${repoPath}" && git init`);
1396
+ shellExec(`mkdir -p "${repoPath}" && git init "${repoPath}"`);
1378
1397
  }
1398
+
1379
1399
  shellExec(`cd "${repoPath}" && git config user.name '${gitUsername}'`);
1380
1400
  shellExec(`cd "${repoPath}" && git config user.email '${gitEmail}'`);
1381
1401
  shellExec(`cd "${repoPath}" && git config core.filemode false`);
@@ -1653,6 +1673,77 @@ Prevent build private config repo.`,
1653
1673
  }
1654
1674
  return fallback;
1655
1675
  },
1676
+
1677
+ /**
1678
+ * Performs a shallow sparse Git checkout of a single subdirectory from any
1679
+ * GitHub repository into a local target directory.
1680
+ *
1681
+ * Uses `--depth 1 --no-checkout` + `git sparse-checkout` so only the
1682
+ * requested path is fetched — no full clone of the remote repo.
1683
+ * Skips the clone entirely when `<targetDir>/<subPath>` already exists on
1684
+ * disk (idempotent).
1685
+ *
1686
+ * Requires `GITHUB_TOKEN` to be set in the environment for authenticated
1687
+ * access to private repositories.
1688
+ *
1689
+ * @param {string} subPath - The subdirectory path within the remote repo to
1690
+ * check out (e.g. `'conf/dd-prototype'`, `'src/api/payments'`).
1691
+ * @param {object} [options]
1692
+ * @param {string} [options.repoOwner='underpostnet'] - GitHub organisation or
1693
+ * user that owns the repository.
1694
+ * @param {string} [options.repoName='engine-private'] - Name of the
1695
+ * repository on GitHub.
1696
+ * @param {string} [options.targetDir='./engine-private'] - Local directory
1697
+ * where the repo will be cloned.
1698
+ * @returns {boolean} `true` when the checkout was performed, `false` when it
1699
+ * was skipped because the target path already existed.
1700
+ * @memberof UnderpostRepository
1701
+ */
1702
+ sparseCheckoutDirectory(
1703
+ subPath,
1704
+ options = { repoOwner: 'underpostnet', repoName: 'engine-private', targetDir: './engine-private' },
1705
+ ) {
1706
+ const { repoOwner = 'underpostnet', repoName = 'engine-private', targetDir = './engine-private' } = options;
1707
+ const localPath = `${targetDir}/${subPath}`;
1708
+ if (fs.existsSync(localPath)) {
1709
+ logger.info('[sparseCheckoutDirectory] path already present, skipping', localPath);
1710
+ return false;
1711
+ }
1712
+ const authUrl = `https://${process.env.GITHUB_TOKEN}@github.com/${repoOwner}/${repoName}.git`;
1713
+ shellExec(`git clone --depth 1 --no-checkout ${authUrl} ${targetDir}`, { disableLog: true });
1714
+ shellExec(`cd ${targetDir} && git sparse-checkout set ${subPath} && git checkout`, { disableLog: true });
1715
+ logger.info('[sparseCheckoutDirectory] sparse checkout complete', localPath);
1716
+ return true;
1717
+ },
1718
+
1719
+ /**
1720
+ * Ensures a deploy's public source repo (e.g. `engine-prototype`) is present
1721
+ * next to the engine and reset to a pristine HEAD, so catalog `sourceMoves`
1722
+ * can (re)pull custom sources even after a previous build moved them out of
1723
+ * the source tree.
1724
+ *
1725
+ * Clones `../<repoName>` when missing; otherwise restores a clean checkout
1726
+ * (`git checkout .` brings back any moved-out tracked files) and pulls latest.
1727
+ * Mirrors the sibling-repo handling used by `syncPrivateConf`.
1728
+ *
1729
+ * @param {string} repoName - Public source repo name (e.g. `engine-prototype`).
1730
+ * @returns {boolean} `true` when the repo is available on disk.
1731
+ * @memberof UnderpostRepository
1732
+ */
1733
+ pullSourceRepo(repoName) {
1734
+ const username = process.env.GITHUB_USERNAME;
1735
+ if (!username || !repoName) return false;
1736
+ const repoPath = `../${repoName}`;
1737
+ const gitUri = `${username}/${repoName}`;
1738
+ if (!fs.existsSync(repoPath)) {
1739
+ shellExec(`cd .. && underpost clone ${gitUri}`, { silent: true });
1740
+ } else {
1741
+ shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
1742
+ silent: true,
1743
+ });
1744
+ }
1745
+ return fs.existsSync(repoPath);
1746
+ },
1656
1747
  };
1657
1748
  }
1658
1749
 
package/src/cli/run.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  getNpmRootPath,
16
16
  isDeployRunnerContext,
17
17
  loadConfServerJson,
18
+ loadReplicas,
18
19
  writeEnv,
19
20
  } from '../server/conf.js';
20
21
  import { actionInitLog, loggerFactory } from '../server/logger.js';
@@ -264,6 +265,16 @@ class UnderpostRun {
264
265
  logger.info(hostListenResult.renderHosts);
265
266
  },
266
267
 
268
+ /**
269
+ * @method etc-hosts
270
+ * @description Modifies the `/etc/hosts` file to add entries for local access to services,
271
+ * based on the provided path input.
272
+ * @param {string} path - The input value, identifier, or path for the operation (used to specify the entries to add to /etc/hosts).
273
+ */
274
+ 'etc-hosts': (path = '', options = DEFAULT_OPTION) => {
275
+ etcHostFactory(path.split(','));
276
+ },
277
+
267
278
  /**
268
279
  * @method ipfs-expose
269
280
  * @description Exposes IPFS Cluster services on specified ports for local access.
@@ -422,6 +433,7 @@ class UnderpostRun {
422
433
  return;
423
434
  }
424
435
  shellExec(`${baseCommand} run pull`);
436
+ shellExec(`${baseCommand} run shared-dir`);
425
437
 
426
438
  // Capture last N commit messages for propagation.
427
439
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -502,6 +514,7 @@ class UnderpostRun {
502
514
  return;
503
515
  }
504
516
  shellExec(`${baseCommand} run pull`);
517
+ shellExec(`${baseCommand} run shared-dir`);
505
518
 
506
519
  // Capture last N commit messages from the engine repo.
507
520
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -713,6 +726,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
713
726
  let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
714
727
  if (targetTraffic) versions = versions ? versions : targetTraffic;
715
728
 
729
+ const ignorePods =
730
+ isDeployRunnerContext(path, options) && targetTraffic
731
+ ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
732
+ : [];
733
+
716
734
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
717
735
  const cmdString = options.cmd
718
736
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
@@ -743,7 +761,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
743
761
  );
744
762
  if (!targetTraffic)
745
763
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
746
- await Underpost.deploy.monitorReadyRunner(deployId, env, targetTraffic, [], options.namespace, 'underpost');
764
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
747
765
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
748
766
  } else
749
767
  logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
@@ -1154,7 +1172,7 @@ EOF
1154
1172
  `,
1155
1173
  { disableLog: true },
1156
1174
  );
1157
- const { ready, readyPods } = await Underpost.deploy.monitorReadyRunner(
1175
+ const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1158
1176
  _deployId,
1159
1177
  env,
1160
1178
  targetTraffic,
@@ -1436,8 +1454,8 @@ EOF`);
1436
1454
  const baseClusterCommand = options.dev ? ' --dev' : '';
1437
1455
  const currentImage = options.imageName
1438
1456
  ? options.imageName
1439
- : Underpost.deploy
1440
- .getCurrentLoadedImages(options.nodeName ? options.nodeName : 'kind-worker', false)
1457
+ : Underpost.image
1458
+ .getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
1441
1459
  .find((o) => o.IMAGE.match('underpost'));
1442
1460
  const podName = options.podName || `underpost-dev-container`;
1443
1461
  const volumeHostPath = options.claimName || '/home/dd';
@@ -1714,20 +1732,13 @@ EOF`);
1714
1732
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1715
1733
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1716
1734
  const env = options.dev ? 'development' : 'production';
1717
- const ignorePods = Underpost.deploy
1735
+ const ignorePods = Underpost.kubectl
1718
1736
  .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
1719
1737
  .map((p) => p.NAME);
1720
1738
 
1721
1739
  shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
1722
1740
 
1723
- await Underpost.deploy.monitorReadyRunner(
1724
- deployId,
1725
- env,
1726
- targetTraffic,
1727
- ignorePods,
1728
- options.namespace,
1729
- 'underpost',
1730
- );
1741
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
1731
1742
 
1732
1743
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
1733
1744
  },
@@ -1819,7 +1830,7 @@ EOF`);
1819
1830
  }`;
1820
1831
  shellExec(cmd, { async: true });
1821
1832
  }
1822
- await awaitDeployMonitor();
1833
+ if ((await awaitDeployMonitor()) !== true) return;
1823
1834
  {
1824
1835
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1825
1836
 
@@ -1827,7 +1838,7 @@ EOF`);
1827
1838
  async: true,
1828
1839
  });
1829
1840
  }
1830
- await awaitDeployMonitor();
1841
+ if ((await awaitDeployMonitor()) !== true) return;
1831
1842
  shellExec(
1832
1843
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1833
1844
  );
@@ -2504,7 +2515,8 @@ EOF`;
2504
2515
  /**
2505
2516
  * @method push-bundle
2506
2517
  * @description Builds the client zip for the specified deployment, splits it into parts, and uploads to file storage.
2507
- * Steps: set env, build+split zip, switch to cron env, upload parts to storage.
2518
+ * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
2519
+ * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2508
2520
  * @param {string} path - Optional `fsPath.splitOption` string.
2509
2521
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2510
2522
  * @param {Object} options - The default underpost runner options for customizing workflow.
@@ -2513,7 +2525,7 @@ EOF`;
2513
2525
  * @memberof UnderpostRun
2514
2526
  */
2515
2527
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2516
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2528
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2517
2529
  const env = options.dev ? 'development' : 'production';
2518
2530
  const deployId = options.deployId || 'dd-default';
2519
2531
  const pathParts = (path || '').split('.');
@@ -2537,11 +2549,54 @@ EOF`;
2537
2549
  }
2538
2550
  }
2539
2551
 
2552
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2553
+ const confServer = fs.existsSync(confServerPath)
2554
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2555
+ : {};
2556
+ const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
2557
+
2540
2558
  shellExec(`${baseCommand} env ${deployId} ${env}`);
2541
2559
  shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
2542
- shellExec(
2543
- `${baseCommand} fs ${fsPath} --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --force`,
2544
- );
2560
+
2561
+ const pushBundleFiles = (host, routePath) => {
2562
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2563
+ const buildDir = `./${fsPath}`;
2564
+ if (!fs.existsSync(buildDir)) return;
2565
+ const partFiles = fs
2566
+ .readdirSync(buildDir)
2567
+ .filter(
2568
+ (name) =>
2569
+ name.startsWith(`${buildId}.zip.part`) ||
2570
+ name.startsWith(`${buildId}.zip-part`) ||
2571
+ name === `${buildId}.zip`,
2572
+ )
2573
+ .map((name) => `${fsPath}/${name}`);
2574
+ if (partFiles.length === 0) {
2575
+ logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
2576
+ return;
2577
+ }
2578
+ for (const partFile of partFiles) {
2579
+ shellExec(
2580
+ `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
2581
+ );
2582
+ }
2583
+ };
2584
+
2585
+ for (const host of Object.keys(confServer)) {
2586
+ for (const routePath of Object.keys(confServer[host])) {
2587
+ const routeConf = confServer[host][routePath] || {};
2588
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2589
+ if (routeConf.singleReplica) {
2590
+ if (routeConf.replicas) {
2591
+ for (const replica of routeConf.replicas) {
2592
+ pushBundleFiles(host, replica);
2593
+ }
2594
+ }
2595
+ continue;
2596
+ }
2597
+ pushBundleFiles(host, routePath);
2598
+ }
2599
+ }
2545
2600
  },
2546
2601
 
2547
2602
  /**
@@ -2558,11 +2613,13 @@ EOF`;
2558
2613
  * @memberof UnderpostRun
2559
2614
  */
2560
2615
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
2561
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2616
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2562
2617
  const env = options.dev ? 'development' : 'production';
2563
2618
  const deployId = options.deployId || 'dd-default';
2564
2619
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2565
- const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
2620
+ const confServer = fs.existsSync(confServerPath)
2621
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2622
+ : {};
2566
2623
  const hostsArg = path
2567
2624
  ? path
2568
2625
  .split(',')
@@ -2581,64 +2638,75 @@ EOF`;
2581
2638
  `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2582
2639
  );
2583
2640
 
2641
+ const pullBundleRoute = (host, routePath) => {
2642
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2643
+ const zipPath = `build/${buildId}.zip`;
2644
+ const buildDir = './build';
2645
+ const hasZip = fs.existsSync(zipPath);
2646
+ const hasParts =
2647
+ fs.existsSync(buildDir) &&
2648
+ fs
2649
+ .readdirSync(buildDir)
2650
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2651
+
2652
+ if (!hasZip && !hasParts) {
2653
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2654
+ return;
2655
+ }
2656
+
2657
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2658
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
2659
+ shellExec(`sudo rm -rf ${zipPath}`);
2660
+
2661
+ if (fs.existsSync(buildDir)) {
2662
+ fs.readdirSync(buildDir)
2663
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2664
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2665
+ }
2666
+
2667
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2668
+ if (!fs.existsSync(extractedDir)) {
2669
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2670
+ return;
2671
+ }
2672
+
2673
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2674
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2675
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2676
+ fs.copySync(`${extractedDir}`, `${publicDestPath}`);
2677
+ };
2678
+
2584
2679
  for (const host of hostsArg) {
2585
- // Gather all routes for this host; fall back to root '/' when host is not in confServer
2586
- // (e.g. when hosts were provided explicitly via the path argument).
2587
2680
  const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2588
2681
 
2589
2682
  for (const routePath of routePaths) {
2590
2683
  const routeConf = confServer[host] ? confServer[host][routePath] || {} : {};
2591
- // Skip routes that are not built by buildClient (mirrors buildClient skip conditions)
2592
- if (routeConf.singleReplica || routeConf.redirect || routeConf.disabledRebuild) continue;
2593
-
2594
- // buildClient names the zip as "<host>-<path-no-slashes>.zip"
2595
- // e.g. host="underpost.net", path="/" → buildId="underpost.net-", zip="build/underpost.net-.zip"
2596
- // e.g. host="app.net", path="/admin" → buildId="app.net-admin", zip="build/app.net-admin.zip"
2597
- const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2598
- const zipPath = `build/${buildId}.zip`;
2599
- const buildDir = './build';
2600
- const hasZip = fs.existsSync(zipPath);
2601
- const hasParts =
2602
- fs.existsSync(buildDir) &&
2603
- fs
2604
- .readdirSync(buildDir)
2605
- .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2606
-
2607
- if (!hasZip && !hasParts) {
2608
- logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2609
- continue;
2610
- }
2611
-
2612
- if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2613
- shellExec(`${baseCommand} client --unzip ${zipPath}`);
2614
- shellExec(`sudo rm -rf ${zipPath}`);
2615
-
2616
- // Clean up downloaded part wrapper zips left by --omit-unzip pull
2617
- if (fs.existsSync(buildDir)) {
2618
- fs.readdirSync(buildDir)
2619
- .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2620
- .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2621
- }
2622
-
2623
- // unzipClientBuild extracts to buildId with trailing '-' stripped
2624
- // e.g. "build/underpost.net-" → "build/underpost.net"
2625
- // e.g. "build/app.net-admin" → "build/app.net-admin" (no trailing dash, no change)
2626
- const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2627
- if (!fs.existsSync(extractedDir)) {
2628
- logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2684
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2685
+ if (routeConf.singleReplica) {
2686
+ if (routeConf.replicas) {
2687
+ for (const replica of routeConf.replicas) {
2688
+ pullBundleRoute(host, replica);
2689
+ }
2690
+ }
2629
2691
  continue;
2630
2692
  }
2631
-
2632
- // Destination mirrors the public directory layout used by the server
2633
- const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2634
- if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2635
- // Ensure parent directory exists for sub-paths
2636
- if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2637
- shellExec(`sudo mv ${extractedDir} ${publicDestPath}`);
2693
+ pullBundleRoute(host, routePath);
2638
2694
  }
2639
2695
  }
2640
2696
  },
2641
2697
 
2698
+ /**
2699
+ * @method build-cluster-deployment-manifests
2700
+ * @description Builds deployment manifests for both production and development environments using `node bin deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment.
2701
+ * @param {string} path - Unused.
2702
+ * @param {Object} options - The default underpost runner options for customizing workflow.
2703
+ * @memberof UnderpostRun
2704
+ */
2705
+ 'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) => {
2706
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
2707
+ shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production --cert`);
2708
+ },
2709
+
2642
2710
  /**
2643
2711
  * @method monitor-ui
2644
2712
  * @description Installs and enables the Cockpit KVM Dashboard (cockpit, cockpit-machines, libvirt)
@@ -86,7 +86,8 @@ class MongooseDBService {
86
86
 
87
87
  const user = config.user || process.env.DB_USER || '';
88
88
  const password = config.password || process.env.DB_PASSWORD || '';
89
- const directConnection = hosts.length === 1;
89
+ const hasExplicitReplicaSet = !!(config.replicaSet || process.env.DB_REPLICA_SET);
90
+ const directConnection = hosts.length === 1 && !hasExplicitReplicaSet;
90
91
  const replicaSet = directConnection
91
92
  ? ''
92
93
  : config.replicaSet || process.env.DB_REPLICA_SET || MONGODB_DEFAULT_REPLICA_SET;
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.12';
47
+ static version = 'v3.2.21';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -3,7 +3,7 @@ FROM rockylinux:9
3
3
  # System packages
4
4
  RUN dnf -y update && \
5
5
  dnf -y install epel-release && \
6
- dnf -y install --allowerasing \
6
+ dnf -y install --allowerasing --nobest \
7
7
  bzip2 \
8
8
  sudo \
9
9
  curl \
@@ -20,8 +20,8 @@ RUN dnf -y update && \
20
20
  perl && \
21
21
  dnf clean all
22
22
 
23
- # Download and install XAMPP (PHP 8.2)
24
- RUN curl -L -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
23
+ # Download and install XAMPP (PHP 8.2) with retry logic for flaky SourceForge transfers
24
+ RUN curl -L --retry 5 --retry-delay 10 --retry-max-time 180 -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
25
25
  chmod +x /tmp/xampp-linux-installer.run && \
26
26
  bash -c "/tmp/xampp-linux-installer.run --mode unattended" && \
27
27
  ln -sf /opt/lampp/lampp /usr/bin/lampp
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Underpost platform content catalog — the base `pwa-microservices-template`.
3
+ *
4
+ * @module src/server/catalog-underpost.js
5
+ * @namespace UnderpostCatalog
6
+ */
7
+
8
+ /**
9
+ * Workflow + service files re-added to the template after the engine-only strip.
10
+ * @constant {string[]}
11
+ * @memberof UnderpostCatalog
12
+ */
13
+ const TEMPLATE_RESTORE_PATHS = [
14
+ `./src/server/catalog-underpost.js`,
15
+ `./.github/workflows/pwa-microservices-template-page.cd.yml`,
16
+ `./.github/workflows/pwa-microservices-template-test.ci.yml`,
17
+ `./.github/workflows/npmpkg.ci.yml`,
18
+ `./.github/workflows/ghpkg.ci.yml`,
19
+ `./.github/workflows/gitlab.ci.yml`,
20
+ `./.github/workflows/publish.ci.yml`,
21
+ `./.github/workflows/release.cd.yml`,
22
+ `./src/client/services/user/guest.service.js`,
23
+ './src/api/user/guest.service.js',
24
+ './src/ws/IoInterface.js',
25
+ './src/ws/IoServer.js',
26
+ './manifests/deployment/dd-default-development',
27
+ ];
28
+
29
+ /**
30
+ * npm keywords for the standalone Underpost platform / template package.
31
+ * @constant {string[]}
32
+ * @memberof UnderpostCatalog
33
+ */
34
+ const TEMPLATE_KEYWORDS = [
35
+ 'underpost',
36
+ 'underpost-platform',
37
+ 'cli',
38
+ 'toolchain',
39
+ 'ci-cd',
40
+ 'devops',
41
+ 'kubernetes',
42
+ 'k3s',
43
+ 'kubeadm',
44
+ 'lxd',
45
+ 'baremetal',
46
+ 'container-orchestration',
47
+ 'image-management',
48
+ 'pwa',
49
+ 'workbox',
50
+ 'microservices',
51
+ ];
52
+
53
+ /**
54
+ * npm description for the standalone Underpost platform / template package.
55
+ * @constant {string}
56
+ * @memberof UnderpostCatalog
57
+ */
58
+ const TEMPLATE_DESCRIPTION =
59
+ 'Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orchestration. Extensible via downstream CLIs.';
60
+
61
+ export { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION };