underpost 3.2.11 → 3.2.14

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.
@@ -556,7 +556,7 @@ class UnderpostRepository {
556
556
  // Handle sync-conf operation
557
557
  if (options.syncConf) {
558
558
  logger.info(`Syncing configuration for deploy ID: ${deployId}`);
559
- shellExec(`node bin/build ${deployId} conf`);
559
+ shellExec(`node bin/build ${deployId} --conf`);
560
560
  logger.info('Configuration synced successfully');
561
561
  return resolve(true);
562
562
  }
@@ -852,6 +852,7 @@ class UnderpostRepository {
852
852
  }
853
853
  }
854
854
  await buildClient({
855
+ deployId: resolvedDeployId,
855
856
  buildZip: options.buildZip || false,
856
857
  split: options.split || '',
857
858
  fullBuild: options.liteBuild ? false : true,
@@ -862,7 +863,12 @@ class UnderpostRepository {
862
863
  logger.warn('Skip replica client build: replica folder not found', { replicaDeployId });
863
864
  continue;
864
865
  }
865
- await Underpost.repo.client(replicaDeployId);
866
+ await Underpost.repo.client(replicaDeployId, '', '', '', {
867
+ buildZip: options.buildZip || false,
868
+ split: options.split || '',
869
+ liteBuild: options.liteBuild || false,
870
+ iconsBuild: options.iconsBuild || false,
871
+ });
866
872
  }
867
873
 
868
874
  return resolve(true);
@@ -937,7 +943,7 @@ Prevent build private config repo.`,
937
943
  deployVersion: packageJsonDeploy.version,
938
944
  };
939
945
  }
940
- shellExec(`node bin/build ${deployId} conf`);
946
+ shellExec(`node bin/build ${deployId} --conf`);
941
947
  return {
942
948
  validVersion: true,
943
949
  engineVersion: packageJsonEngine.version,
@@ -1653,6 +1659,77 @@ Prevent build private config repo.`,
1653
1659
  }
1654
1660
  return fallback;
1655
1661
  },
1662
+
1663
+ /**
1664
+ * Performs a shallow sparse Git checkout of a single subdirectory from any
1665
+ * GitHub repository into a local target directory.
1666
+ *
1667
+ * Uses `--depth 1 --no-checkout` + `git sparse-checkout` so only the
1668
+ * requested path is fetched — no full clone of the remote repo.
1669
+ * Skips the clone entirely when `<targetDir>/<subPath>` already exists on
1670
+ * disk (idempotent).
1671
+ *
1672
+ * Requires `GITHUB_TOKEN` to be set in the environment for authenticated
1673
+ * access to private repositories.
1674
+ *
1675
+ * @param {string} subPath - The subdirectory path within the remote repo to
1676
+ * check out (e.g. `'conf/dd-prototype'`, `'src/api/payments'`).
1677
+ * @param {object} [options]
1678
+ * @param {string} [options.repoOwner='underpostnet'] - GitHub organisation or
1679
+ * user that owns the repository.
1680
+ * @param {string} [options.repoName='engine-private'] - Name of the
1681
+ * repository on GitHub.
1682
+ * @param {string} [options.targetDir='./engine-private'] - Local directory
1683
+ * where the repo will be cloned.
1684
+ * @returns {boolean} `true` when the checkout was performed, `false` when it
1685
+ * was skipped because the target path already existed.
1686
+ * @memberof UnderpostRepository
1687
+ */
1688
+ sparseCheckoutDirectory(
1689
+ subPath,
1690
+ options = { repoOwner: 'underpostnet', repoName: 'engine-private', targetDir: './engine-private' },
1691
+ ) {
1692
+ const { repoOwner = 'underpostnet', repoName = 'engine-private', targetDir = './engine-private' } = options;
1693
+ const localPath = `${targetDir}/${subPath}`;
1694
+ if (fs.existsSync(localPath)) {
1695
+ logger.info('[sparseCheckoutDirectory] path already present, skipping', localPath);
1696
+ return false;
1697
+ }
1698
+ const authUrl = `https://${process.env.GITHUB_TOKEN}@github.com/${repoOwner}/${repoName}.git`;
1699
+ shellExec(`git clone --depth 1 --no-checkout ${authUrl} ${targetDir}`, { disableLog: true });
1700
+ shellExec(`cd ${targetDir} && git sparse-checkout set ${subPath} && git checkout`, { disableLog: true });
1701
+ logger.info('[sparseCheckoutDirectory] sparse checkout complete', localPath);
1702
+ return true;
1703
+ },
1704
+
1705
+ /**
1706
+ * Ensures a deploy's public source repo (e.g. `engine-prototype`) is present
1707
+ * next to the engine and reset to a pristine HEAD, so catalog `sourceMoves`
1708
+ * can (re)pull custom sources even after a previous build moved them out of
1709
+ * the source tree.
1710
+ *
1711
+ * Clones `../<repoName>` when missing; otherwise restores a clean checkout
1712
+ * (`git checkout .` brings back any moved-out tracked files) and pulls latest.
1713
+ * Mirrors the sibling-repo handling used by `syncPrivateConf`.
1714
+ *
1715
+ * @param {string} repoName - Public source repo name (e.g. `engine-prototype`).
1716
+ * @returns {boolean} `true` when the repo is available on disk.
1717
+ * @memberof UnderpostRepository
1718
+ */
1719
+ pullSourceRepo(repoName) {
1720
+ const username = process.env.GITHUB_USERNAME;
1721
+ if (!username || !repoName) return false;
1722
+ const repoPath = `../${repoName}`;
1723
+ const gitUri = `${username}/${repoName}`;
1724
+ if (!fs.existsSync(repoPath)) {
1725
+ shellExec(`cd .. && underpost clone ${gitUri}`, { silent: true });
1726
+ } else {
1727
+ shellExec(`cd ${repoPath} && git checkout . && git clean -f -d && underpost pull . ${gitUri}`, {
1728
+ silent: true,
1729
+ });
1730
+ }
1731
+ return fs.existsSync(repoPath);
1732
+ },
1656
1733
  };
1657
1734
  }
1658
1735
 
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';
@@ -422,6 +423,7 @@ class UnderpostRun {
422
423
  return;
423
424
  }
424
425
  shellExec(`${baseCommand} run pull`);
426
+ shellExec(`${baseCommand} run shared-dir`);
425
427
 
426
428
  // Capture last N commit messages for propagation.
427
429
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -502,6 +504,7 @@ class UnderpostRun {
502
504
  return;
503
505
  }
504
506
  shellExec(`${baseCommand} run pull`);
507
+ shellExec(`${baseCommand} run shared-dir`);
505
508
 
506
509
  // Capture last N commit messages from the engine repo.
507
510
  // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
@@ -713,6 +716,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
713
716
  let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
714
717
  if (targetTraffic) versions = versions ? versions : targetTraffic;
715
718
 
719
+ const ignorePods =
720
+ isDeployRunnerContext(path, options) && targetTraffic
721
+ ? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
722
+ : [];
723
+
716
724
  const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
717
725
  const cmdString = options.cmd
718
726
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
@@ -743,7 +751,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
743
751
  );
744
752
  if (!targetTraffic)
745
753
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
746
- await Underpost.deploy.monitorReadyRunner(deployId, env, targetTraffic, [], options.namespace, 'underpost');
754
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
747
755
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, replicas, options.namespace, options);
748
756
  } else
749
757
  logger.info('current traffic', Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }));
@@ -1154,7 +1162,7 @@ EOF
1154
1162
  `,
1155
1163
  { disableLog: true },
1156
1164
  );
1157
- const { ready, readyPods } = await Underpost.deploy.monitorReadyRunner(
1165
+ const { ready, readyPods } = await Underpost.monitor.monitorReadyRunner(
1158
1166
  _deployId,
1159
1167
  env,
1160
1168
  targetTraffic,
@@ -1436,8 +1444,8 @@ EOF`);
1436
1444
  const baseClusterCommand = options.dev ? ' --dev' : '';
1437
1445
  const currentImage = options.imageName
1438
1446
  ? options.imageName
1439
- : Underpost.deploy
1440
- .getCurrentLoadedImages(options.nodeName ? options.nodeName : 'kind-worker', false)
1447
+ : Underpost.image
1448
+ .getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', false)
1441
1449
  .find((o) => o.IMAGE.match('underpost'));
1442
1450
  const podName = options.podName || `underpost-dev-container`;
1443
1451
  const volumeHostPath = options.claimName || '/home/dd';
@@ -1714,20 +1722,13 @@ EOF`);
1714
1722
  const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
1715
1723
  const targetTraffic = currentTraffic === 'blue' ? 'green' : 'blue';
1716
1724
  const env = options.dev ? 'development' : 'production';
1717
- const ignorePods = Underpost.deploy
1725
+ const ignorePods = Underpost.kubectl
1718
1726
  .get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace)
1719
1727
  .map((p) => p.NAME);
1720
1728
 
1721
1729
  shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${options.namespace}`);
1722
1730
 
1723
- await Underpost.deploy.monitorReadyRunner(
1724
- deployId,
1725
- env,
1726
- targetTraffic,
1727
- ignorePods,
1728
- options.namespace,
1729
- 'underpost',
1730
- );
1731
+ await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, options.namespace);
1731
1732
 
1732
1733
  Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, options.namespace, options);
1733
1734
  },
@@ -1819,7 +1820,7 @@ EOF`);
1819
1820
  }`;
1820
1821
  shellExec(cmd, { async: true });
1821
1822
  }
1822
- await awaitDeployMonitor(true);
1823
+ if ((await awaitDeployMonitor()) !== true) return;
1823
1824
  {
1824
1825
  const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
1825
1826
 
@@ -1827,7 +1828,7 @@ EOF`);
1827
1828
  async: true,
1828
1829
  });
1829
1830
  }
1830
- await awaitDeployMonitor(true);
1831
+ if ((await awaitDeployMonitor()) !== true) return;
1831
1832
  shellExec(
1832
1833
  `NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
1833
1834
  );
@@ -2504,7 +2505,8 @@ EOF`;
2504
2505
  /**
2505
2506
  * @method push-bundle
2506
2507
  * @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.
2508
+ * Steps: set env, build+split zip, upload only the zip parts belonging to the deploy-id's hosts (from conf.server.json).
2509
+ * Only files matching `<host>-<route>.zip.part*` or `<host>-<route>.zip` for each non-skipped route are uploaded.
2508
2510
  * @param {string} path - Optional `fsPath.splitOption` string.
2509
2511
  * Examples: `build` (default split 8), `build.16` (split 16 MB), `build.none-split` (no split flag).
2510
2512
  * @param {Object} options - The default underpost runner options for customizing workflow.
@@ -2513,7 +2515,7 @@ EOF`;
2513
2515
  * @memberof UnderpostRun
2514
2516
  */
2515
2517
  'push-bundle': (path = '', options = DEFAULT_OPTION) => {
2516
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2518
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2517
2519
  const env = options.dev ? 'development' : 'production';
2518
2520
  const deployId = options.deployId || 'dd-default';
2519
2521
  const pathParts = (path || '').split('.');
@@ -2537,11 +2539,54 @@ EOF`;
2537
2539
  }
2538
2540
  }
2539
2541
 
2542
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2543
+ const confServer = fs.existsSync(confServerPath)
2544
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2545
+ : {};
2546
+ const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
2547
+
2540
2548
  shellExec(`${baseCommand} env ${deployId} ${env}`);
2541
2549
  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
- );
2550
+
2551
+ const pushBundleFiles = (host, routePath) => {
2552
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2553
+ const buildDir = `./${fsPath}`;
2554
+ if (!fs.existsSync(buildDir)) return;
2555
+ const partFiles = fs
2556
+ .readdirSync(buildDir)
2557
+ .filter(
2558
+ (name) =>
2559
+ name.startsWith(`${buildId}.zip.part`) ||
2560
+ name.startsWith(`${buildId}.zip-part`) ||
2561
+ name === `${buildId}.zip`,
2562
+ )
2563
+ .map((name) => `${fsPath}/${name}`);
2564
+ if (partFiles.length === 0) {
2565
+ logger.warn(`push-bundle: no bundle files found for '${host}${routePath}'`, { buildId });
2566
+ return;
2567
+ }
2568
+ for (const partFile of partFiles) {
2569
+ shellExec(
2570
+ `${baseCommand} fs ${partFile} --deploy-id ${deployId} --storage-file-path ${storageFilePath} --force`,
2571
+ );
2572
+ }
2573
+ };
2574
+
2575
+ for (const host of Object.keys(confServer)) {
2576
+ for (const routePath of Object.keys(confServer[host])) {
2577
+ const routeConf = confServer[host][routePath] || {};
2578
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2579
+ if (routeConf.singleReplica) {
2580
+ if (routeConf.replicas) {
2581
+ for (const replica of routeConf.replicas) {
2582
+ pushBundleFiles(host, replica);
2583
+ }
2584
+ }
2585
+ continue;
2586
+ }
2587
+ pushBundleFiles(host, routePath);
2588
+ }
2589
+ }
2545
2590
  },
2546
2591
 
2547
2592
  /**
@@ -2558,11 +2603,13 @@ EOF`;
2558
2603
  * @memberof UnderpostRun
2559
2604
  */
2560
2605
  'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
2561
- const baseCommand = options.dev ? 'node bin' : 'underpost';
2606
+ const baseCommand = 'node bin'; // options.dev ? 'node bin' : 'underpost';
2562
2607
  const env = options.dev ? 'development' : 'production';
2563
2608
  const deployId = options.deployId || 'dd-default';
2564
2609
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
2565
- const confServer = fs.existsSync(confServerPath) ? loadConfServerJson(confServerPath) : {};
2610
+ const confServer = fs.existsSync(confServerPath)
2611
+ ? loadReplicas(deployId, loadConfServerJson(confServerPath))
2612
+ : {};
2566
2613
  const hostsArg = path
2567
2614
  ? path
2568
2615
  .split(',')
@@ -2581,60 +2628,59 @@ EOF`;
2581
2628
  `${baseCommand} fs build --recursive --deploy-id ${deployId} --storage-file-path engine-private/conf/${deployId}/storage.bundle.json --pull --omit-unzip`,
2582
2629
  );
2583
2630
 
2631
+ const pullBundleRoute = (host, routePath) => {
2632
+ const buildId = `${host}-${routePath.replaceAll('/', '')}`;
2633
+ const zipPath = `build/${buildId}.zip`;
2634
+ const buildDir = './build';
2635
+ const hasZip = fs.existsSync(zipPath);
2636
+ const hasParts =
2637
+ fs.existsSync(buildDir) &&
2638
+ fs
2639
+ .readdirSync(buildDir)
2640
+ .some((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`));
2641
+
2642
+ if (!hasZip && !hasParts) {
2643
+ logger.warn(`Bundle not found for '${host}${routePath}'. Skipping.`, { zipPath, deployId });
2644
+ return;
2645
+ }
2646
+
2647
+ if (hasParts) shellExec(`${baseCommand} client --merge-zip ${zipPath}`);
2648
+ shellExec(`${baseCommand} client --unzip ${zipPath}`);
2649
+ shellExec(`sudo rm -rf ${zipPath}`);
2650
+
2651
+ if (fs.existsSync(buildDir)) {
2652
+ fs.readdirSync(buildDir)
2653
+ .filter((name) => name.startsWith(`${buildId}.zip.part`) || name.startsWith(`${buildId}.zip-part`))
2654
+ .forEach((partFile) => shellExec(`sudo rm -rf ${buildDir}/${partFile}`));
2655
+ }
2656
+
2657
+ const extractedDir = `build/${buildId.replace(/-$/, '')}`;
2658
+ if (!fs.existsSync(extractedDir)) {
2659
+ logger.warn(`Extracted build dir not found: ${extractedDir}. Skipping move for '${host}${routePath}'.`);
2660
+ return;
2661
+ }
2662
+
2663
+ const publicDestPath = routePath === '/' ? `public/${host}` : `public/${host}${routePath}`;
2664
+ if (fs.existsSync(publicDestPath)) shellExec(`sudo rm -rf ${publicDestPath}`);
2665
+ if (routePath !== '/') shellExec(`sudo mkdir -p public/${host}`);
2666
+ fs.copySync(`${extractedDir}`, `${publicDestPath}`);
2667
+ };
2668
+
2584
2669
  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
2670
  const routePaths = confServer[host] ? Object.keys(confServer[host]) : ['/'];
2588
2671
 
2589
2672
  for (const routePath of routePaths) {
2590
2673
  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}'.`);
2674
+ if (routeConf.redirect || routeConf.disabledRebuild) continue;
2675
+ if (routeConf.singleReplica) {
2676
+ if (routeConf.replicas) {
2677
+ for (const replica of routeConf.replicas) {
2678
+ pullBundleRoute(host, replica);
2679
+ }
2680
+ }
2629
2681
  continue;
2630
2682
  }
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}`);
2683
+ pullBundleRoute(host, routePath);
2638
2684
  }
2639
2685
  }
2640
2686
  },
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.11';
47
+ static version = 'v3.2.14';
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 };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Dynamic product-catalog resolver.
3
+ *
4
+ * Product catalogs (`catalog-<suffix>.js`, e.g. `catalog-cyberia`, `catalog-prototype`)
5
+ * are loaded lazily by deploy id via ES dynamic `import()` so the base build
6
+ * (`bin/build`) and template assembly (`bin/build.template`) never statically
7
+ * depend on any product module. Removing a product catalog simply makes its
8
+ * deploy id resolve to the empty catalog — nothing else breaks.
9
+ *
10
+ * Each product catalog default-exports the uniform shape documented in
11
+ * {@link module:src/server/catalog-cyberia}.
12
+ *
13
+ * @module src/server/catalog.js
14
+ * @namespace Catalog
15
+ */
16
+
17
+ import fs from 'fs-extra';
18
+ import { fileURLToPath } from 'node:url';
19
+ import * as path from 'node:path';
20
+
21
+ const catalogDir = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ /** Empty product catalog returned for deploy ids without a dedicated module. */
24
+ const EMPTY_CATALOG = {
25
+ sourceMoves: [],
26
+ privateConfPaths: [],
27
+ templatePaths: [],
28
+ stripPaths: [],
29
+ keywords: [],
30
+ description: '',
31
+ };
32
+
33
+ /**
34
+ * Loads a single deploy id's product catalog. The suffix after `dd-` selects the
35
+ * module (`dd-cyberia` → `catalog-cyberia.js`). Returns {@link EMPTY_CATALOG} when
36
+ * the deploy id has no dedicated catalog or the module cannot be loaded.
37
+ *
38
+ * @method loadDeployCatalog
39
+ * @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`).
40
+ * @returns {Promise<object>} The product catalog (uniform shape).
41
+ * @memberof Catalog
42
+ */
43
+ const loadDeployCatalog = async (deployId) => {
44
+ const suffix = (deployId ?? '').split('dd-')[1];
45
+ if (!suffix) return EMPTY_CATALOG;
46
+ try {
47
+ const mod = await import(`./catalog-${suffix}.js`);
48
+ return { ...EMPTY_CATALOG, ...(mod.default ?? {}) };
49
+ } catch {
50
+ return EMPTY_CATALOG;
51
+ }
52
+ };
53
+
54
+ /**
55
+ * Loads every product catalog present alongside this module (`catalog-*.js`,
56
+ * excluding the base `catalog-underpost` and this resolver). Used to aggregate
57
+ * product `stripPaths` for the base template without naming any product.
58
+ *
59
+ * @method loadProductCatalogs
60
+ * @returns {Promise<object[]>} Loaded product catalogs (uniform shape).
61
+ * @memberof Catalog
62
+ */
63
+ const loadProductCatalogs = async () => {
64
+ const catalogs = [];
65
+ for (const file of fs.readdirSync(catalogDir)) {
66
+ if (!/^catalog-.+\.js$/.test(file) || file === 'catalog-underpost.js') continue;
67
+ try {
68
+ const mod = await import(`./${file}`);
69
+ if (mod.default) catalogs.push({ ...EMPTY_CATALOG, ...mod.default });
70
+ } catch {
71
+ /* a malformed/removed product catalog must not break the base build */
72
+ }
73
+ }
74
+ return catalogs;
75
+ };
76
+
77
+ export { loadDeployCatalog, loadProductCatalogs, EMPTY_CATALOG };