underpost 3.2.21 → 3.2.28

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.
Files changed (55) hide show
  1. package/.github/workflows/ghpkg.ci.yml +1 -1
  2. package/.github/workflows/gitlab.ci.yml +1 -1
  3. package/.github/workflows/npmpkg.ci.yml +1 -1
  4. package/.github/workflows/publish.ci.yml +2 -2
  5. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
  6. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  7. package/CHANGELOG.md +178 -1
  8. package/CLI-HELP.md +58 -2
  9. package/README.md +3 -2
  10. package/baremetal/commission-workflows.json +1 -0
  11. package/bin/build.js +13 -1
  12. package/docker-compose.yml +224 -0
  13. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  14. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/package.json +27 -15
  17. package/scripts/kubeadm-node-setup.sh +317 -0
  18. package/scripts/rocky-kickstart.sh +877 -185
  19. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  20. package/scripts/test-monitor.sh +242 -78
  21. package/src/cli/baremetal.js +717 -16
  22. package/src/cli/cluster.js +3 -1
  23. package/src/cli/deploy.js +11 -10
  24. package/src/cli/docker-compose.js +591 -0
  25. package/src/cli/fs.js +35 -11
  26. package/src/cli/index.js +83 -0
  27. package/src/cli/kickstart.js +142 -20
  28. package/src/cli/monitor.js +114 -29
  29. package/src/cli/repository.js +75 -11
  30. package/src/cli/run.js +275 -6
  31. package/src/cli/ssh.js +190 -0
  32. package/src/cli/static.js +2 -2
  33. package/src/client/components/core/PanelForm.js +44 -44
  34. package/src/{server → client-builder}/client-build-docs.js +15 -5
  35. package/src/{server → client-builder}/client-build-live.js +3 -3
  36. package/src/{server → client-builder}/client-build.js +25 -22
  37. package/src/{server → client-builder}/client-dev-server.js +3 -3
  38. package/src/{server → client-builder}/client-icons.js +2 -2
  39. package/src/{server → client-builder}/ssr.js +5 -5
  40. package/src/client.build.js +1 -1
  41. package/src/client.dev.js +1 -1
  42. package/src/index.js +12 -1
  43. package/src/mailer/EmailRender.js +1 -1
  44. package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
  45. package/src/runtime/express/Express.js +2 -2
  46. package/src/runtime/nginx/Nginx.js +250 -0
  47. package/src/server/catalog.js +7 -14
  48. package/src/server/conf.js +61 -12
  49. package/src/server/start.js +17 -5
  50. package/src/server.js +1 -1
  51. package/test/deploy-monitor.test.js +33 -5
  52. package/typedoc.json +3 -1
  53. package/src/server/ipfs-client.js +0 -597
  54. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  55. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Exported singleton instance of the NginxService class.
3
+ * Manages dynamic generation of nginx reverse-proxy router configuration used
4
+ * by the Docker Compose development stack. Mirrors the conventions of
5
+ * {@link module:src/runtime/lampp/Lampp.js LamppService}.
6
+ * @module src/runtime/nginx/Nginx.js
7
+ * @namespace NginxService
8
+ */
9
+
10
+ import fs from 'fs-extra';
11
+ import path from 'path';
12
+ import { loggerFactory } from '../../server/logger.js';
13
+
14
+ const logger = loggerFactory(import.meta);
15
+
16
+ /**
17
+ * @class NginxService
18
+ * @description Builds nginx `server` blocks (the router) for fronting upstream
19
+ * application services and writes the rendered configuration to disk. The
20
+ * router is accumulated in memory via {@link NginxService#createApp} and
21
+ * flushed with {@link NginxService#writeConf}, keeping config generation
22
+ * decoupled from the filesystem location it is written to.
23
+ * @memberof NginxService
24
+ */
25
+ class NginxService {
26
+ /**
27
+ * @type {string}
28
+ * @description Accumulated nginx `server { ... }` blocks (the router definition).
29
+ * @memberof NginxService
30
+ */
31
+ router = '';
32
+
33
+ /**
34
+ * @type {Set<string>}
35
+ * @description Upstream blocks keyed by upstream name to avoid duplicates.
36
+ * @memberof NginxService
37
+ */
38
+ upstreams;
39
+
40
+ /**
41
+ * @type {boolean}
42
+ * @description Whether a default_server catch-all block has been emitted.
43
+ * @memberof NginxService
44
+ */
45
+ hasDefaultServer;
46
+
47
+ constructor() {
48
+ this.reset();
49
+ }
50
+
51
+ /**
52
+ * Resets the in-memory router, upstreams, and default-server flag.
53
+ * @method reset
54
+ * @returns {void}
55
+ * @memberof NginxService
56
+ */
57
+ reset() {
58
+ this.router = '';
59
+ this.upstreams = new Map();
60
+ this.hasDefaultServer = false;
61
+ }
62
+
63
+ /**
64
+ * Appends a raw render fragment to the router string.
65
+ * @method appendRouter
66
+ * @param {string} render - The configuration fragment to append.
67
+ * @returns {string} The complete, updated router configuration string.
68
+ * @memberof NginxService
69
+ */
70
+ appendRouter(render) {
71
+ if (!this.router) return (this.router = render);
72
+ return (this.router += render);
73
+ }
74
+
75
+ /**
76
+ * Clears the in-memory router configuration.
77
+ * @method removeRouter
78
+ * @returns {void}
79
+ * @memberof NginxService
80
+ */
81
+ removeRouter() {
82
+ this.reset();
83
+ }
84
+
85
+ /**
86
+ * Registers a named upstream pointing at a container service:port.
87
+ * Idempotent: repeated names with the same target are collapsed.
88
+ * @method addUpstream
89
+ * @param {string} name - The upstream identifier.
90
+ * @param {string} service - The Docker service name (resolved via service discovery).
91
+ * @param {number} port - The upstream container port.
92
+ * @returns {string} The upstream name.
93
+ * @memberof NginxService
94
+ */
95
+ addUpstream(name, service, port) {
96
+ this.upstreams.set(name, `upstream ${name} { server ${service}:${port}; }`);
97
+ return name;
98
+ }
99
+
100
+ /**
101
+ * Renders the standard proxy directive block shared by all locations,
102
+ * including websocket upgrade headers and forwarded headers.
103
+ * @method proxyLocation
104
+ * @param {string} location - The location path (e.g. '/', '/peer').
105
+ * @param {string} upstream - The upstream name to proxy to.
106
+ * @returns {string} The rendered `location { ... }` block.
107
+ * @memberof NginxService
108
+ */
109
+ proxyLocation(location, upstream) {
110
+ return `
111
+ location ${location} {
112
+ proxy_pass http://${upstream};
113
+ proxy_set_header Host $host;
114
+ proxy_set_header X-Real-IP $remote_addr;
115
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
116
+ proxy_set_header X-Forwarded-Proto $scheme;
117
+ proxy_set_header Upgrade $http_upgrade;
118
+ proxy_set_header Connection $connection_upgrade;
119
+ proxy_read_timeout 3600s;
120
+ }
121
+ `;
122
+ }
123
+
124
+ /**
125
+ * Renders the `/healthz` location used by the proxy container healthcheck.
126
+ * @method healthLocation
127
+ * @returns {string} The rendered health-check location block.
128
+ * @memberof NginxService
129
+ */
130
+ healthLocation() {
131
+ return `
132
+ location = /healthz {
133
+ access_log off;
134
+ return 200 "ok\\n";
135
+ add_header Content-Type text/plain;
136
+ }
137
+ `;
138
+ }
139
+
140
+ /**
141
+ * Creates an nginx virtual-host (`server`) entry for a host and appends it to
142
+ * the router. Each route maps a URL prefix to an upstream service:port with
143
+ * websocket support, mirroring the Contour HTTPProxy route model.
144
+ *
145
+ * @method createApp
146
+ * @param {object} options - Virtual host options.
147
+ * @param {string} options.host - The `server_name` (e.g. 'default.net').
148
+ * @param {number} [options.listen=80] - The listen port.
149
+ * @param {Array<{location: string, service: string, port: number}>} options.routes
150
+ * - Route table. Longer prefixes should precede '/' for correct matching.
151
+ * @param {boolean} [options.resetRouter] - Clear the router before appending.
152
+ * @returns {string} The complete, updated router configuration string.
153
+ * @memberof NginxService
154
+ */
155
+ createApp({ host, listen = 80, routes = [], resetRouter = false }) {
156
+ if (resetRouter) this.removeRouter();
157
+
158
+ const safeHost = host.replace(/[^a-zA-Z0-9]/g, '_');
159
+ const locationBlocks = routes
160
+ .map(({ location, service, port }) => {
161
+ const upstreamName = this.addUpstream(`up_${safeHost}_${port}`, service, port);
162
+ return this.proxyLocation(location, upstreamName);
163
+ })
164
+ .join('');
165
+
166
+ this.appendRouter(`
167
+ server {
168
+ listen ${listen};
169
+ server_name ${host};
170
+ ${this.healthLocation()}${locationBlocks}}
171
+ `);
172
+
173
+ return this.router;
174
+ }
175
+
176
+ /**
177
+ * Emits a `default_server` catch-all that forwards to the given upstream and
178
+ * answers the health probe for unmatched Host headers. Safe to call once.
179
+ *
180
+ * @method createDefaultServer
181
+ * @param {object} options - Default server options.
182
+ * @param {string} options.service - The Docker service name to fall back to.
183
+ * @param {number} options.port - The upstream container port.
184
+ * @param {number} [options.listen=80] - The listen port.
185
+ * @returns {string} The complete, updated router configuration string.
186
+ * @memberof NginxService
187
+ */
188
+ createDefaultServer({ service, port, listen = 80 }) {
189
+ if (this.hasDefaultServer) return this.router;
190
+ this.hasDefaultServer = true;
191
+ const upstreamName = this.addUpstream('up_default', service, port);
192
+ this.appendRouter(`
193
+ server {
194
+ listen ${listen} default_server;
195
+ server_name _;
196
+ ${this.healthLocation()}${this.proxyLocation('/', upstreamName)}}
197
+ `);
198
+ return this.router;
199
+ }
200
+
201
+ /**
202
+ * Renders the full nginx config document: the websocket connection map, all
203
+ * upstream blocks, the global `proxy_http_version`, and the accumulated router.
204
+ * @method render
205
+ * @returns {string} The complete nginx configuration file content.
206
+ * @memberof NginxService
207
+ */
208
+ render() {
209
+ const upstreamBlocks = Array.from(this.upstreams.values()).join('\n');
210
+ return `# Generated by src/runtime/nginx/Nginx.js — do not hand-edit.
211
+ # Reverse proxy derived from manifests/deployment/*/proxy.yaml (Contour HTTPProxy).
212
+ # Upstreams resolve container services via the Docker internal network.
213
+
214
+ map $http_upgrade $connection_upgrade {
215
+ default upgrade;
216
+ '' close;
217
+ }
218
+
219
+ ${upstreamBlocks}
220
+
221
+ proxy_http_version 1.1;
222
+ ${this.router}`;
223
+ }
224
+
225
+ /**
226
+ * Writes the rendered configuration to the given path, creating parent
227
+ * directories as needed. Idempotent and safe to rerun.
228
+ * @method writeConf
229
+ * @param {string} confPath - Absolute or relative destination path.
230
+ * @returns {string} The path written.
231
+ * @memberof NginxService
232
+ */
233
+ writeConf(confPath) {
234
+ const target = path.resolve(confPath);
235
+ fs.mkdirpSync(path.dirname(target));
236
+ fs.writeFileSync(target, this.render(), 'utf8');
237
+ logger.info(`nginx config written`, { path: target, upstreams: this.upstreams.size });
238
+ return target;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * @description Exported singleton instance of the NginxService class.
244
+ * @type {NginxService}
245
+ * @memberof NginxService
246
+ */
247
+ const Nginx = new NginxService();
248
+
249
+ export { Nginx, NginxService };
250
+ export default Nginx;
@@ -18,8 +18,6 @@ import fs from 'fs-extra';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import * as path from 'node:path';
20
20
 
21
- const catalogDir = path.dirname(fileURLToPath(import.meta.url));
22
-
23
21
  /** Empty product catalog returned for deploy ids without a dedicated module. */
24
22
  const EMPTY_CATALOG = {
25
23
  sourceMoves: [],
@@ -43,12 +41,11 @@ const EMPTY_CATALOG = {
43
41
  const loadDeployCatalog = async (deployId) => {
44
42
  const suffix = (deployId ?? '').split('dd-')[1];
45
43
  if (!suffix) return EMPTY_CATALOG;
46
- try {
47
- const mod = await import(`./catalog-${suffix}.js`);
44
+ if (fs.existsSync(`./src/projects/${suffix}/catalog-${suffix}.js`)) {
45
+ const mod = await import(`../projects/${suffix}/catalog-${suffix}.js`);
48
46
  return { ...EMPTY_CATALOG, ...(mod.default ?? {}) };
49
- } catch {
50
- return EMPTY_CATALOG;
51
47
  }
48
+ return EMPTY_CATALOG;
52
49
  };
53
50
 
54
51
  /**
@@ -62,14 +59,10 @@ const loadDeployCatalog = async (deployId) => {
62
59
  */
63
60
  const loadProductCatalogs = async () => {
64
61
  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
- }
62
+ for (const file of await fs.readdir('./src/projects')) {
63
+ if (file === 'underpost') continue;
64
+ const mod = await import(`../projects/${file}/catalog-${file}.js`);
65
+ if (mod.default) catalogs.push({ ...EMPTY_CATALOG, ...mod.default });
73
66
  }
74
67
  return catalogs;
75
68
  };
@@ -1337,7 +1337,7 @@ const mergeFile = async (parts = [], outputFilePath) => {
1337
1337
  * @memberof ServerConfBuilder
1338
1338
  */
1339
1339
  const getPathsSSR = (conf) => {
1340
- const paths = ['src/client/ssr/Render.js'];
1340
+ const paths = ['src/client/ssr/RootDocument.js'];
1341
1341
  for (const o of conf.head) paths.push(`src/client/ssr/head/${o}.js`);
1342
1342
  for (const o of conf.body) paths.push(`src/client/ssr/body/${o}.js`);
1343
1343
  for (const o of Object.keys(conf.mailer)) paths.push(`src/client/ssr/mailer/${conf.mailer[o]}.js`);
@@ -1856,6 +1856,7 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
1856
1856
  if (!fs.existsSync(privateRepoPath)) {
1857
1857
  shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
1858
1858
  } else {
1859
+ shellExec(`git config --global --add safe.directory '${dir.resolve(privateRepoPath)}'`);
1859
1860
  shellExec(`cd ${privateRepoPath} && git checkout . && git clean -f -d && underpost pull . ${privateGitUri}`, {
1860
1861
  silent: true,
1861
1862
  });
@@ -1927,7 +1928,8 @@ const syncDeployIdSources = (sourceMoves = []) => {
1927
1928
  */
1928
1929
  const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
1929
1930
  const walk = (await import('ignore-walk')).default;
1930
- const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } = await import('./catalog-underpost.js');
1931
+ const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
1932
+ await import('../projects/underpost/catalog-underpost.js');
1931
1933
  const { loadProductCatalogs } = await import('./catalog.js');
1932
1934
  const githubUsername = process.env.GITHUB_USERNAME;
1933
1935
 
@@ -1941,15 +1943,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
1941
1943
  )
1942
1944
  ).filter((p) => !p.startsWith('.git'));
1943
1945
 
1944
- // Clone the template from 0 if missing; otherwise reset it to a clean pristine checkout.
1945
- if (!fs.existsSync(toPath)) {
1946
- shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
1947
- } else {
1948
- shellExec(`cd ${toPath} && git reset && git checkout . && git clean -f -d`);
1949
- shellExec(`node bin pull ${toPath} ${githubUsername}/pwa-microservices-template`);
1950
- shellExec(`sudo rm -rf ${toPath}/engine-private`);
1951
- shellExec(`sudo rm -rf ${toPath}/logs`);
1952
- }
1946
+ fs.removeSync(`${githubUsername}/pwa-microservices-template`);
1947
+ shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
1948
+
1953
1949
  shellExec(`cd ${toPath} && git config core.filemode false`);
1954
1950
 
1955
1951
  for (const copyPath of sourceFiles) {
@@ -1979,7 +1975,6 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
1979
1975
  }
1980
1976
  shellExec(`rm -rf ${toPath}/.github`);
1981
1977
  shellExec(`rm -rf ${toPath}/manifests/deployment/dd-*`);
1982
- shellExec(`rm -rf ${toPath}/src/server/catalog-*`);
1983
1978
 
1984
1979
  fs.mkdirSync(`${toPath}/.github/workflows`, { recursive: true });
1985
1980
  for (const restorePath of TEMPLATE_RESTORE_PATHS) {
@@ -2052,6 +2047,59 @@ git add .`);
2052
2047
  }
2053
2048
  };
2054
2049
 
2050
+ /**
2051
+ * @method updatePrivateEngineTestRepo
2052
+ * @description Publishes a deploy id's freshly assembled template to its private
2053
+ * **test** source repo `engine-test-<idPart>` (separate from the production
2054
+ * `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
2055
+ * clones this repo, so work-in-progress engine source can be tested end to end
2056
+ * without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
2057
+ * but per-deploy-id and against the test repo.
2058
+ *
2059
+ * Assumes the deploy id template has already been assembled at the template path
2060
+ * (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
2061
+ * @param {string} deployId - Concrete deploy id (e.g. `dd-core`).
2062
+ * @returns {Promise<void>}
2063
+ * @memberof ServerConfBuilder
2064
+ */
2065
+ const updatePrivateEngineTestRepo = async (deployId) => {
2066
+ const username = process.env.GITHUB_USERNAME || 'underpostnet';
2067
+ const repoName = `engine-test-${deployId.split('-')[1]}`;
2068
+ const templatePath = '/home/dd/pwa-microservices-template';
2069
+ if (!fs.existsSync(templatePath))
2070
+ throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
2071
+
2072
+ // Detach the assembled working tree from any engine-build git history.
2073
+ shellExec(`sudo rm -rf ${templatePath}/.git`);
2074
+
2075
+ // Adopt the test repo's existing history when present (so the push is a delta);
2076
+ // otherwise publish a fresh history on first push.
2077
+ shellExec(`cd /home/dd && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
2078
+ silent: true,
2079
+ disableLog: true,
2080
+ silentOnError: true,
2081
+ });
2082
+ if (fs.existsSync(`/home/dd/${repoName}.git`)) shellExec(`mv /home/dd/${repoName}.git ${templatePath}/.git`);
2083
+
2084
+ // `git init` converts the moved bare repo into a normal work-tree repo (bare
2085
+ // clones have no work tree, so `git add` would fail), and bootstraps a fresh
2086
+ // repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
2087
+ shellExec(`cd ${templatePath}
2088
+ git init
2089
+ git config user.name '${username}'
2090
+ git config user.email 'development@underpost.net'
2091
+ git add .`);
2092
+
2093
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2094
+ stdout: true,
2095
+ silent: true,
2096
+ disableLog: true,
2097
+ }).trim();
2098
+ if (hasChanges === '1')
2099
+ shellExec(`cd ${templatePath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
2100
+ else logger.info('No changes to publish', { repoName });
2101
+ };
2102
+
2055
2103
  export {
2056
2104
  Config,
2057
2105
  loadConf,
@@ -2104,4 +2152,5 @@ export {
2104
2152
  syncDeployIdSources,
2105
2153
  buildTemplate,
2106
2154
  updatePrivateTemplateRepo,
2155
+ updatePrivateEngineTestRepo,
2107
2156
  };
@@ -152,10 +152,16 @@ class UnderpostStartUp {
152
152
  // observable through every lifecycle phase, including build and init. Bind
153
153
  // the deployment-resolved port so it always matches the monitor's target.
154
154
  startInternalStatusServer(deployStatusPort(deployId, env));
155
- setRuntimeStatus(deployId, env, RUNTIME_STATUS.BUILD);
156
- if (options.build === true) await Underpost.start.build(deployId, env, options);
157
- setRuntimeStatus(deployId, env, RUNTIME_STATUS.INIT);
158
- if (options.run === true) await Underpost.start.run(deployId, env, options);
155
+ try {
156
+ setRuntimeStatus(deployId, env, RUNTIME_STATUS.BUILD);
157
+ if (options.build === true) await Underpost.start.build(deployId, env, options);
158
+ setRuntimeStatus(deployId, env, RUNTIME_STATUS.INIT);
159
+ if (options.run === true) await Underpost.start.run(deployId, env, options);
160
+ } catch (error) {
161
+ logger.error('Deployment build/init failed', { deployId, env, message: error?.message });
162
+ setRuntimeStatus(deployId, env, RUNTIME_STATUS.ERROR);
163
+ if (!Underpost.env.isInsideContainer()) throw error;
164
+ }
159
165
  },
160
166
  /**
161
167
  * Run itc-scripts and builds client bundle.
@@ -167,6 +173,8 @@ class UnderpostStartUp {
167
173
  * @param {boolean} options.skipFullBuild - Whether to skip building the full client bundle.
168
174
  * @param {boolean} options.pullBundle - When true, download pre-built client bundle from Cloudinary via pull-bundle (must be pushed first with push-bundle).
169
175
  * This flag is independent of skipFullBuild: it can be combined with skipFullBuild or used alone.
176
+ * @param {boolean} options.privateTestRepo - When true, clone `engine-test-<id>` (the private test source repo
177
+ * published by `node bin/build <deployId> --update-private`) instead of the production `engine-<id>` repo.
170
178
  * @memberof UnderpostStartUp
171
179
  */
172
180
  async build(
@@ -175,7 +183,11 @@ class UnderpostStartUp {
175
183
  options = { underpostQuicklyInstall: false, skipPullBase: false, skipFullBuild: false, pullBundle: false },
176
184
  ) {
177
185
  const buildBasePath = `/home/dd`;
178
- const repoName = `engine-${deployId.split('-')[1]}`;
186
+ // `--private-test-repo` clones the isolated test source repo published by
187
+ // `node bin/build <deployId> --update-private`, instead of the production one.
188
+ const repoName = options?.privateTestRepo
189
+ ? `engine-test-${deployId.split('-')[1]}`
190
+ : `engine-${deployId.split('-')[1]}`;
179
191
  if (!options.skipPullBase) {
180
192
  shellExec(`cd ${buildBasePath} && underpost clone ${process.env.GITHUB_USERNAME}/${repoName}`);
181
193
  shellExec(`mkdir -p ${buildBasePath}/engine`);
package/src/server.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // https://expressjs.com/en/4x/api.html
5
5
 
6
6
  import { loggerFactory } from './server/logger.js';
7
- import { buildClient } from './server/client-build.js';
7
+ import { buildClient } from './client-builder/client-build.js';
8
8
  import { buildRuntime } from './server/runtime.js';
9
9
  import { ProcessController } from './server/process.js';
10
10
  import { Config } from './server/conf.js';
@@ -108,6 +108,10 @@ if [[ "$verb" == "port-forward" ]]; then
108
108
  sleep 30
109
109
  exit 0
110
110
  fi
111
+ if [[ "$verb" == "exec" ]]; then
112
+ grep -E '^container-status=' "$UNDERPOST_ENV_FILE" 2>/dev/null | tail -n1 | sed -E 's/^container-status=//'
113
+ exit 0
114
+ fi
111
115
  exit 0
112
116
  `,
113
117
  );
@@ -118,10 +122,13 @@ exit 0
118
122
  fs.writeFileSync(
119
123
  monitorScriptPath,
120
124
  `import Underpost from ${JSON.stringify(pathToFileURL(path.join(repoRoot, 'src/index.js')).href)};
125
+ const options = {};
126
+ if (process.env.MON_READY_GATE) options.readyGate = process.env.MON_READY_GATE;
127
+ if (process.env.MON_TRANSPORT) options.statusTransport = process.env.MON_TRANSPORT;
121
128
  try {
122
129
  await Underpost.monitor.monitorReadyRunner(${JSON.stringify(DEPLOY_ID)}, ${JSON.stringify(ENV)}, ${JSON.stringify(
123
130
  TRAFFIC,
124
- )}, [], 'default');
131
+ )}, [], 'default', options);
125
132
  process.exit(0);
126
133
  } catch (_) {
127
134
  process.exit(1);
@@ -174,12 +181,18 @@ try {
174
181
  });
175
182
  });
176
183
 
177
- it('success: both phases satisfied → monitor exits 0', async () => {
184
+ it('success (default exec transport): both phases satisfied → monitor exits 0', async () => {
178
185
  Underpost.env.set('container-status', RUNNING_STATUS);
179
186
  const code = await spawnMonitor({ FAKE_POD_READY: 'True' });
180
187
  expect(code).to.equal(0);
181
188
  });
182
189
 
190
+ it('success (opt-in http transport): both phases satisfied → monitor exits 0', async () => {
191
+ Underpost.env.set('container-status', RUNNING_STATUS);
192
+ const code = await spawnMonitor({ FAKE_POD_READY: 'True', MON_TRANSPORT: 'http' });
193
+ expect(code).to.equal(0);
194
+ });
195
+
183
196
  it('runtime failure: container-status=error → monitor exits 1', async () => {
184
197
  const monitorExit = spawnMonitor({ FAKE_POD_READY: 'True' });
185
198
  Underpost.env.set('container-status', 'error');
@@ -193,11 +206,12 @@ try {
193
206
  expect(code).to.equal(1);
194
207
  });
195
208
 
196
- it('transport failure: endpoint unreachable is never success (exits 1)', async () => {
197
- // Point the monitor at a port with no internal server; the HTTP read always
198
- // fails, so runtime readiness is never confirmed and the monitor times out.
209
+ it('transport failure (http): endpoint unreachable is never success (exits 1)', async () => {
210
+ // Opt into http and point the monitor at a port with no internal server; the
211
+ // HTTP read always fails, so runtime readiness is never confirmed timeout.
199
212
  Underpost.env.set('container-status', RUNNING_STATUS);
200
213
  const code = await spawnMonitor({
214
+ MON_TRANSPORT: 'http',
201
215
  UNDERPOST_INTERNAL_PORT: String(CLOSED_PORT),
202
216
  UNDERPOST_PF_LOCAL_PORT: String(CLOSED_PORT),
203
217
  UNDERPOST_MONITOR_MAX_ITERATIONS: '3',
@@ -220,4 +234,18 @@ try {
220
234
  Underpost.env.set('container-status', BUILD_STATUS);
221
235
  expect(await monitorExit).to.equal(1);
222
236
  });
237
+
238
+ // Custom instances (cyberia-*) gate on K8s Ready and read status via exec;
239
+ // their runtime never stamps `running-deployment` (stays `initializing`).
240
+ it('instance (kubernetes gate + exec): K8s Ready with initializing status → exits 0', async () => {
241
+ Underpost.env.set('container-status', INIT_STATUS);
242
+ const code = await spawnMonitor({ FAKE_POD_READY: 'True', MON_READY_GATE: 'kubernetes', MON_TRANSPORT: 'exec' });
243
+ expect(code).to.equal(0);
244
+ });
245
+
246
+ it('instance (kubernetes gate + exec): container-status=error → exits 1', async () => {
247
+ const monitorExit = spawnMonitor({ FAKE_POD_READY: 'True', MON_READY_GATE: 'kubernetes', MON_TRANSPORT: 'exec' });
248
+ Underpost.env.set('container-status', 'error');
249
+ expect(await monitorExit).to.equal(1);
250
+ });
223
251
  });
package/typedoc.json CHANGED
@@ -8,7 +8,9 @@
8
8
  "./src/ws",
9
9
  "./src/grpc",
10
10
  "./src/mailer",
11
- "./src/runtime"
11
+ "./src/runtime",
12
+ "./src/client-builder",
13
+ "./src/projects/underpost"
12
14
  ],
13
15
  "entryPointStrategy": "expand",
14
16
  "exclude": ["**/node_modules/**", "**/docs/**", "**/client/**"],