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.
package/src/cli/deploy.js CHANGED
@@ -20,7 +20,6 @@ import { loggerFactory } from '../server/logger.js';
20
20
  import { shellExec } from '../server/process.js';
21
21
  import fs from 'fs-extra';
22
22
  import dotenv from 'dotenv';
23
- import { timer } from '../client/components/core/CommonJs.js';
24
23
  import os from 'node:os';
25
24
  import Underpost from '../index.js';
26
25
 
@@ -827,59 +826,6 @@ EOF`);
827
826
  }
828
827
  }
829
828
  },
830
- /**
831
- * Checks the status of a deployment.
832
- * @param {string} deployId - Deployment ID for which the status is being checked.
833
- * @param {string} env - Environment for which the status is being checked.
834
- * @param {string} traffic - Current traffic status for the deployment.
835
- * @param {Array<string>} ignoresNames - List of pod names to ignore.
836
- * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
837
- * @returns {object} - Object containing the status of the deployment.
838
- * @memberof UnderpostDeploy
839
- */
840
- async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
841
- const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
842
- const readyPods = [];
843
- const notReadyPods = [];
844
-
845
- // Readiness signal: the pod's Kubernetes `Ready` condition driven by the
846
- // container's readinessProbe (TCP socket, HTTP get, or exec). Set by kubelet
847
- // when the probe passes. A failed or crashing runtime never becomes Ready —
848
- // kubelet surfaces CrashLoopBackOff and this gate stays closed.
849
- for (const pod of pods) {
850
- const { NAME } = pod;
851
- if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
852
-
853
- let podJson = null;
854
- try {
855
- // Pod may not exist yet (between deployment apply and pod
856
- // scheduling). silentOnError lets the monitor loop continue
857
- // instead of aborting on the transient NotFound exit.
858
- const raw = shellExec(`sudo kubectl get pod ${NAME} -n ${namespace} -o json`, {
859
- silent: true,
860
- disableLog: true,
861
- stdout: true,
862
- silentOnError: true,
863
- });
864
- podJson = raw ? JSON.parse(raw) : null;
865
- } catch (_) {
866
- podJson = null;
867
- }
868
- const conditions = podJson?.status?.conditions || [];
869
- const readyCondition = conditions.find((c) => c.type === 'Ready');
870
- const k8sReady = readyCondition?.status === 'True';
871
-
872
- pod.out = JSON.stringify({ k8sReady, condition: readyCondition ?? null });
873
-
874
- if (k8sReady) readyPods.push(pod);
875
- else notReadyPods.push(pod);
876
- }
877
- return {
878
- ready: pods.length > 0 && notReadyPods.length === 0,
879
- notReadyPods,
880
- readyPods,
881
- };
882
- },
883
829
  /**
884
830
  * Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
885
831
  * Secrets are mounted as tmpfs (never written to node disk) and support RBAC restrictions.
@@ -1139,180 +1085,6 @@ spec:
1139
1085
  env === 'production' &&
1140
1086
  options.cert === true &&
1141
1087
  (!options.certHosts || options.certHosts.split(',').includes(host)),
1142
- /**
1143
- * Monitors the ready status of a deployment.
1144
- *
1145
- * Ready signal:
1146
- * The orchestrator gate is the Kubernetes pod Ready condition. When the
1147
- * container's `readinessProbe` succeeds, kubelet flips
1148
- * `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
1149
- * returns the pod in `readyPods`. This is the only required signal — see
1150
- * `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
1151
- *
1152
- * Container-status:
1153
- * `underpost config get container-status` is read from each pod for both
1154
- * the display column and as a second ready gate alongside the K8S Ready
1155
- * condition. Both must be satisfied before the monitor exits:
1156
- * 1. K8S readinessProbe (TCP socket) — ensures the port is bound.
1157
- * 2. container-status == `<deploy>-<env>-running-deployment` — ensures
1158
- * the application has completed its own startup sequence.
1159
- * Early-abort on `error` container-status remains in effect.
1160
- *
1161
- * @param {string} deployId - Deployment ID for which the ready status is being monitored.
1162
- * @param {string} env - Environment for which the ready status is being monitored.
1163
- * @param {string} targetTraffic - Target traffic status for the deployment.
1164
- * @param {Array<string>} ignorePods - List of pod names to ignore.
1165
- * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
1166
- * @returns {object} - Object containing the ready status of the deployment.
1167
- * @memberof UnderpostDeploy
1168
- */
1169
- async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
1170
- const delayMs = 1000;
1171
- const maxIterations = 3000;
1172
- const deploymentId = `${deployId}-${env}-${targetTraffic}`;
1173
- const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
1174
- const tag = `[${deploymentId}]`;
1175
- const containerStatusDefault = 'waiting for status';
1176
-
1177
- logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
1178
-
1179
- // Per-pod cache of last-known container-status (persists across retries)
1180
- const podStatusCache = new Map();
1181
-
1182
- const readContainerStatus = (podName) => {
1183
- try {
1184
- const raw = shellExec(
1185
- `sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
1186
- { silent: true, disableLog: true, stdout: true, silentOnError: true },
1187
- );
1188
- const val = raw ? raw.toString().trim() : '';
1189
- return val && val !== 'undefined' ? val : containerStatusDefault;
1190
- } catch (_) {
1191
- // exec failed (e.g. pod not yet running) — preserve last known value
1192
- return podStatusCache.get(podName) || containerStatusDefault;
1193
- }
1194
- };
1195
-
1196
- for (let i = 0; i < maxIterations; i++) {
1197
- const result = await Underpost.deploy.checkDeploymentReadyStatus(
1198
- deployId,
1199
- env,
1200
- targetTraffic,
1201
- ignorePods,
1202
- namespace,
1203
- );
1204
-
1205
- const allPods = [...result.readyPods, ...result.notReadyPods];
1206
-
1207
- // Update cache with latest status for each pod (informational + error gate)
1208
- for (const pod of allPods) {
1209
- if (!pod?.NAME) continue;
1210
- const status = readContainerStatus(pod.NAME);
1211
- if (status === 'error') throw new Error(`Pod ${pod.NAME} has error status`);
1212
- podStatusCache.set(pod.NAME, status);
1213
- }
1214
-
1215
- const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
1216
-
1217
- const allPodsStatusReady =
1218
- allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
1219
-
1220
- // Print snapshot for every pod — annotate when container-status hasn't caught
1221
- // up to the K8S Ready condition yet.
1222
- for (const pod of allPods) {
1223
- const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
1224
- const podStatus = pod.STATUS || 'Unknown';
1225
- const statusMatchesExpected = status === expectedContainerStatus;
1226
- const statusDisplay = statusMatchesExpected ? status : `${status} (pending)`;
1227
-
1228
- console.log(
1229
- 'Target pod:',
1230
- pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
1231
- '| Pod status:',
1232
- podStatus.bold.yellow,
1233
- '| Runtime status:',
1234
- statusDisplay.bold.cyan,
1235
- );
1236
- }
1237
-
1238
- // Both K8S readinessProbe AND container-status must be satisfied before
1239
- // declaring the deployment ready. The TCP probe ensures the port is bound;
1240
- // container-status == running-deployment ensures the application has
1241
- // completed its own startup sequence so traffic is not switched prematurely.
1242
- if (allPodsK8sReady && allPodsStatusReady) {
1243
- logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
1244
- return result;
1245
- }
1246
-
1247
- await timer(delayMs);
1248
-
1249
- if ((i + 1) % 10 === 0) {
1250
- logger.info(`${tag} | In progress... iteration ${i + 1}`);
1251
- }
1252
- }
1253
-
1254
- logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
1255
- throw new Error(
1256
- `monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
1257
- );
1258
- },
1259
-
1260
- /**
1261
- * Retrieves the currently loaded images in the Kubernetes cluster.
1262
- * @param {string} [node='kind-worker'] - Node name to check for loaded images.
1263
- * @param {object} options - Options for the image retrieval.
1264
- * @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
1265
- * @param {string} options.namespace - Kubernetes namespace to filter pods.
1266
- * @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
1267
- * @memberof UnderpostDeploy
1268
- */
1269
- getCurrentLoadedImages(node = 'kind-worker', options = { spec: false, namespace: '' }) {
1270
- if (options.spec) {
1271
- const raw = shellExec(
1272
- `kubectl get pods ${options.namespace ? `--namespace ${options.namespace}` : `--all-namespaces`} -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.namespace}{"/"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
1273
- {
1274
- stdout: true,
1275
- silent: true,
1276
- },
1277
- );
1278
- return raw
1279
- .split(`\n`)
1280
- .map((lines) => ({
1281
- pod: lines.split('\t')[0].replaceAll(':', '').trim(),
1282
- image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
1283
- }))
1284
- .filter((o) => o.image);
1285
- }
1286
- const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
1287
- stdout: true,
1288
- silent: true,
1289
- });
1290
-
1291
- const heads = raw
1292
- .split(`\n`)[0]
1293
- .split(' ')
1294
- .filter((_r) => _r.trim());
1295
-
1296
- const pods = raw
1297
- .split(`\n`)
1298
- .filter((r) => !r.match('IMAGE'))
1299
- .map((r) => r.split(' ').filter((_r) => _r.trim()));
1300
-
1301
- const result = [];
1302
-
1303
- for (const row of pods) {
1304
- if (row.length === 0) continue;
1305
- const pod = {};
1306
- let index = -1;
1307
- for (const head of heads) {
1308
- if (head in pod) continue;
1309
- index++;
1310
- pod[head] = row[index];
1311
- }
1312
- result.push(pod);
1313
- }
1314
- return result;
1315
- },
1316
1088
 
1317
1089
  /**
1318
1090
  * Predefined resource templates for Kubernetes deployments.
package/src/cli/image.js CHANGED
@@ -122,6 +122,63 @@ class UnderpostImage {
122
122
  else if (kubeadm === true) shellExec(`sudo ctr -n k8s.io images import ${tarFile}`);
123
123
  else if (k3s === true) shellExec(`sudo k3s ctr images import ${tarFile}`);
124
124
  },
125
+ /**
126
+ * @method getCurrentLoaded
127
+ * @description Retrieves the currently loaded images in the Kubernetes cluster.
128
+ * @param {string} [node='kind-worker'] - Node name to check for loaded images.
129
+ * @param {object} options - Options for the image retrieval.
130
+ * @param {boolean} options.spec - Whether to retrieve images from the pod specifications.
131
+ * @param {string} options.namespace - Kubernetes namespace to filter pods.
132
+ * @returns {Array<object>} - Array of objects containing pod names and their corresponding images.
133
+ * @memberof UnderpostImage
134
+ */
135
+ getCurrentLoaded(node = 'kind-worker', options = { spec: false, namespace: '' }) {
136
+ if (options.spec) {
137
+ const raw = shellExec(
138
+ `kubectl get pods ${options.namespace ? `--namespace ${options.namespace}` : `--all-namespaces`} -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.namespace}{"/"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
139
+ {
140
+ stdout: true,
141
+ silent: true,
142
+ },
143
+ );
144
+ return raw
145
+ .split(`\n`)
146
+ .map((lines) => ({
147
+ pod: lines.split('\t')[0].replaceAll(':', '').trim(),
148
+ image: lines.split('\t')[1] ? lines.split('\t')[1].replaceAll(',', '').trim() : null,
149
+ }))
150
+ .filter((o) => o.image);
151
+ }
152
+ const raw = shellExec(node === 'kind-worker' ? `docker exec -i ${node} crictl images` : `crictl images`, {
153
+ stdout: true,
154
+ silent: true,
155
+ });
156
+
157
+ const heads = raw
158
+ .split(`\n`)[0]
159
+ .split(' ')
160
+ .filter((_r) => _r.trim());
161
+
162
+ const pods = raw
163
+ .split(`\n`)
164
+ .filter((r) => !r.match('IMAGE'))
165
+ .map((r) => r.split(' ').filter((_r) => _r.trim()));
166
+
167
+ const result = [];
168
+
169
+ for (const row of pods) {
170
+ if (row.length === 0) continue;
171
+ const pod = {};
172
+ let index = -1;
173
+ for (const head of heads) {
174
+ if (head in pod) continue;
175
+ index++;
176
+ pod[head] = row[index];
177
+ }
178
+ result.push(pod);
179
+ }
180
+ return result;
181
+ },
125
182
  /**
126
183
  * @method list
127
184
  * @description Lists currently loaded Docker images in the specified Kubernetes cluster node.
@@ -139,10 +196,7 @@ class UnderpostImage {
139
196
  list(options = { nodeName: '', namespace: '', spec: false, log: false, k3s: false, kubeadm: false, kind: false }) {
140
197
  if ((options.kubeadm === true || options.k3s === true) && !options.nodeName)
141
198
  options.nodeName = shellExec('echo $HOSTNAME', { stdout: true, silent: true }).trim();
142
- const list = Underpost.deploy.getCurrentLoadedImages(
143
- options.nodeName ? options.nodeName : 'kind-worker',
144
- options,
145
- );
199
+ const list = Underpost.image.getCurrentLoaded(options.nodeName ? options.nodeName : 'kind-worker', options);
146
200
  if (options.log) console.table(list);
147
201
  return list;
148
202
  },
@@ -12,6 +12,7 @@ import {
12
12
  etcHostFactory,
13
13
  } from '../server/conf.js';
14
14
  import { loggerFactory } from '../server/logger.js';
15
+ import { timer } from '../client/components/core/CommonJs.js';
15
16
  import axios from 'axios';
16
17
  import fs from 'fs-extra';
17
18
  import { shellExec } from '../server/process.js';
@@ -93,13 +94,13 @@ class UnderpostMonitor {
93
94
  }
94
95
 
95
96
  if (options.readyDeployment) {
96
- for (const version of options.versions.split(',')) {
97
- (async () => {
98
- await Underpost.deploy.monitorReadyRunner(deployId, env, version, [], options.namespace, 'underpost');
97
+ await Promise.all(
98
+ options.versions.split(',').map(async (version) => {
99
+ await Underpost.monitor.monitorReadyRunner(deployId, env, version, [], options.namespace);
99
100
  if (options.promote)
100
101
  Underpost.deploy.switchTraffic(deployId, env, version, options.replicas, options.namespace, options);
101
- })();
102
- }
102
+ }),
103
+ );
103
104
  return;
104
105
  }
105
106
 
@@ -227,7 +228,7 @@ class UnderpostMonitor {
227
228
  monitorPodName = undefined;
228
229
  }
229
230
  const checkDeploymentReadyStatus = async () => {
230
- const { ready, notReadyPods, readyPods } = await Underpost.deploy.checkDeploymentReadyStatus(
231
+ const { ready, notReadyPods, readyPods } = await Underpost.monitor.checkDeploymentReadyStatus(
231
232
  deployId,
232
233
  env,
233
234
  traffic,
@@ -272,6 +273,189 @@ class UnderpostMonitor {
272
273
  };
273
274
  return new Promise((...args) => monitorCallBack(...args));
274
275
  },
276
+ /**
277
+ * Checks the status of a deployment.
278
+ * @param {string} deployId - Deployment ID for which the status is being checked.
279
+ * @param {string} env - Environment for which the status is being checked.
280
+ * @param {string} traffic - Current traffic status for the deployment.
281
+ * @param {Array<string>} ignoresNames - List of pod names to ignore.
282
+ * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
283
+ * @returns {object} - Object containing the status of the deployment.
284
+ * @memberof UnderpostMonitor
285
+ */
286
+ async checkDeploymentReadyStatus(deployId, env, traffic, ignoresNames = [], namespace = 'default') {
287
+ const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
288
+ const readyPods = [];
289
+ const notReadyPods = [];
290
+
291
+ // Readiness signal: the pod's Kubernetes `Ready` condition driven by the
292
+ // container's readinessProbe (TCP socket, HTTP get, or exec). Set by kubelet
293
+ // when the probe passes. A failed or crashing runtime never becomes Ready —
294
+ // kubelet surfaces CrashLoopBackOff and this gate stays closed.
295
+ for (const pod of pods) {
296
+ const { NAME } = pod;
297
+ if (ignoresNames && ignoresNames.find((t) => NAME.trim().toLowerCase().match(t.trim().toLowerCase()))) continue;
298
+
299
+ let podJson = null;
300
+ try {
301
+ // Pod may not exist yet (between deployment apply and pod
302
+ // scheduling). silentOnError lets the monitor loop continue
303
+ // instead of aborting on the transient NotFound exit.
304
+ const raw = shellExec(`sudo kubectl get pod ${NAME} -n ${namespace} -o json`, {
305
+ silent: true,
306
+ disableLog: true,
307
+ stdout: true,
308
+ silentOnError: true,
309
+ });
310
+ podJson = raw ? JSON.parse(raw) : null;
311
+ } catch (_) {
312
+ podJson = null;
313
+ }
314
+ const conditions = podJson?.status?.conditions || [];
315
+ const readyCondition = conditions.find((c) => c.type === 'Ready');
316
+ const k8sReady = readyCondition?.status === 'True';
317
+
318
+ pod.out = JSON.stringify({ k8sReady, condition: readyCondition ?? null });
319
+
320
+ if (k8sReady) readyPods.push(pod);
321
+ else notReadyPods.push(pod);
322
+ }
323
+ const consideredCount = readyPods.length + notReadyPods.length;
324
+ return {
325
+ ready: consideredCount > 0 && notReadyPods.length === 0,
326
+ notReadyPods,
327
+ readyPods,
328
+ };
329
+ },
330
+ /**
331
+ * Monitors the ready status of a deployment.
332
+ *
333
+ * Ready signal:
334
+ * The orchestrator gate is the Kubernetes pod Ready condition. When the
335
+ * container's `readinessProbe` succeeds, kubelet flips
336
+ * `status.conditions[Ready]` to True and `checkDeploymentReadyStatus`
337
+ * returns the pod in `readyPods`. This is the only required signal — see
338
+ * `src/client/public/nexodev/docs/references/Deploy custom instance to K8S.md`.
339
+ *
340
+ * Container-status:
341
+ * `underpost config get container-status` is read from each pod for both
342
+ * the display column and as a second ready gate alongside the K8S Ready
343
+ * condition. Both must be satisfied before the monitor exits:
344
+ * 1. K8S readinessProbe (TCP socket) — ensures the port is bound.
345
+ * 2. container-status == `<deploy>-<env>-running-deployment` — ensures
346
+ * the application has completed its own startup sequence.
347
+ * Early-abort on `error` container-status remains in effect: a failing
348
+ * runtime keeps its pod alive (not Ready) with `container-status=error`,
349
+ * so this `exec`-read surfaces the failure and the monitor aborts —
350
+ * failing the CD runner instead of waiting out the full timeout.
351
+ *
352
+ * @param {string} deployId - Deployment ID for which the ready status is being monitored.
353
+ * @param {string} env - Environment for which the ready status is being monitored.
354
+ * @param {string} targetTraffic - Target traffic status for the deployment.
355
+ * @param {Array<string>} ignorePods - List of pod names to ignore.
356
+ * @param {string} [namespace='default'] - Kubernetes namespace for the deployment.
357
+ * @returns {object} - Object containing the ready status of the deployment.
358
+ * @memberof UnderpostMonitor
359
+ */
360
+ async monitorReadyRunner(deployId, env, targetTraffic, ignorePods = [], namespace = 'default') {
361
+ const delayMs = 1000;
362
+ const maxIterations = 3000;
363
+ const deploymentId = `${deployId}-${env}-${targetTraffic}`;
364
+ const expectedContainerStatus = `${deployId}-${env}-running-deployment`;
365
+ const tag = `[${deploymentId}]`;
366
+ const containerStatusDefault = 'waiting for status';
367
+
368
+ logger.info('Deployment init', { deployId, env, targetTraffic, namespace });
369
+
370
+ const podStatusCache = new Map();
371
+ const advancedPods = new Set();
372
+
373
+ const readContainerStatus = (podName) => {
374
+ try {
375
+ const raw = shellExec(
376
+ `sudo kubectl exec ${podName} -n ${namespace} -- sh -c 'underpost config get container-status --plain'`,
377
+ { silent: true, disableLog: true, stdout: true, silentOnError: true },
378
+ );
379
+ const val = raw ? raw.toString().trim() : '';
380
+ return val && val !== 'undefined' ? val : containerStatusDefault;
381
+ } catch (_) {
382
+ // exec failed (e.g. pod not yet running) — preserve last known value
383
+ return podStatusCache.get(podName) || containerStatusDefault;
384
+ }
385
+ };
386
+
387
+ for (let i = 0; i < maxIterations; i++) {
388
+ const result = await Underpost.monitor.checkDeploymentReadyStatus(
389
+ deployId,
390
+ env,
391
+ targetTraffic,
392
+ ignorePods,
393
+ namespace,
394
+ );
395
+
396
+ const allPods = [...result.readyPods, ...result.notReadyPods];
397
+
398
+ for (const pod of allPods) {
399
+ if (!pod?.NAME) continue;
400
+ const podStatus = (pod.STATUS || '').toLowerCase().trim();
401
+ if (
402
+ ['error', 'crashloopbackoff', 'oomkilled', 'imagepullbackoff', 'errimagepull'].find((s) =>
403
+ podStatus.match(s),
404
+ )
405
+ )
406
+ throw new Error(`Pod ${pod.NAME} has error pod status: ${pod.STATUS}`);
407
+ const status = readContainerStatus(pod.NAME);
408
+ if (status === 'error') throw new Error(`Pod ${pod.NAME} has error container-status`);
409
+ if (advancedPods.has(pod.NAME) && status === containerStatusDefault)
410
+ throw new Error(`Pod ${pod.NAME} container-status regressed to default — pod likely restarted`);
411
+ if (status !== containerStatusDefault) advancedPods.add(pod.NAME);
412
+ podStatusCache.set(pod.NAME, status);
413
+ }
414
+
415
+ const allPodsK8sReady = allPods.length > 0 && result.notReadyPods.length === 0;
416
+
417
+ const allPodsStatusReady =
418
+ allPods.length > 0 && allPods.every((pod) => podStatusCache.get(pod.NAME) === expectedContainerStatus);
419
+
420
+ // Print snapshot for every pod — annotate when container-status hasn't caught
421
+ // up to the K8S Ready condition yet.
422
+ for (const pod of allPods) {
423
+ const status = podStatusCache.get(pod.NAME) || containerStatusDefault;
424
+ const podStatus = pod.STATUS || 'Unknown';
425
+ const statusMatchesExpected = status === expectedContainerStatus;
426
+ const statusDisplay = statusMatchesExpected ? status : `${status} (pending)`;
427
+
428
+ console.log(
429
+ 'Target pod:',
430
+ pod.NAME[pod.NAME.includes('green') ? 'bgGreen' : 'bgBlue'].bold.black,
431
+ '| Pod status:',
432
+ podStatus.bold.yellow,
433
+ '| Runtime status:',
434
+ statusDisplay.bold.cyan,
435
+ );
436
+ }
437
+
438
+ // Both K8S readinessProbe AND container-status must be satisfied before
439
+ // declaring the deployment ready. The TCP probe ensures the port is bound;
440
+ // container-status == running-deployment ensures the application has
441
+ // completed its own startup sequence so traffic is not switched prematurely.
442
+ if (allPodsK8sReady && allPodsStatusReady) {
443
+ logger.info(`${tag} | All pods Ready (K8S readinessProbe satisfied)`);
444
+ return result;
445
+ }
446
+
447
+ await timer(delayMs);
448
+
449
+ if ((i + 1) % 10 === 0) {
450
+ logger.info(`${tag} | In progress... iteration ${i + 1}`);
451
+ }
452
+ }
453
+
454
+ logger.error(`${tag} | Deployment timeout after ${maxIterations} iterations`);
455
+ throw new Error(
456
+ `monitorReadyRunner timeout: ${deploymentId} did not become Ready within ${maxIterations}*${delayMs}ms`,
457
+ );
458
+ },
275
459
  };
276
460
  }
277
461
 
@@ -268,7 +268,7 @@ async function buildAndTestTemplate() {
268
268
  killDevServers();
269
269
  Underpost.repo.clean({ paths: ['/home/dd/engine', '/home/dd/engine/engine-private '] });
270
270
  shellExec(`node bin pull . ${process.env.GITHUB_USERNAME}/engine`);
271
- shellExec(`npm run update:template`);
271
+ shellExec(`npm run build:template`);
272
272
  shellExec(`node bin run shared-dir ${TEMPLATE_PATH}`);
273
273
 
274
274
  const dhcpHostIp = Dns.getLocalIPv4Address();
@@ -392,7 +392,7 @@ class UnderpostRelease {
392
392
  shellExec(`node bin/build dd`);
393
393
  shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd production`);
394
394
  shellExec(`node bin deploy --build-manifest --sync --info-router --replicas 1 dd development`);
395
- shellExec(`node bin/deploy build-default-confs`);
395
+ shellExec(`node bin new --default-conf --conf-workflow-id template`);
396
396
  shellExec(`sudo rm -rf ./engine-private/conf/dd-default`);
397
397
  shellExec(`node bin new --deploy-id dd-default`);
398
398
  console.log(fs.existsSync(`./engine-private/conf/dd-default`));
@@ -460,7 +460,7 @@ class UnderpostRelease {
460
460
  * Runs the pwa-microservices-template update and push flow locally.
461
461
  *
462
462
  * Always removes and re-clones pwa-microservices-template, then:
463
- * 1. Runs update:template (node bin/build.template) to sync engine sources.
463
+ * 1. Runs build:template (node bin/build.template) to sync engine sources.
464
464
  * 2. Installs dependencies and builds the template.
465
465
  * 3. Commits and pushes to the pwa-microservices-template remote repository.
466
466
  *
@@ -488,7 +488,7 @@ class UnderpostRelease {
488
488
  shellExec(`sudo rm -rf /home/dd/pwa-microservices-template`);
489
489
  shellExec(`node engine/bin clone ${githubOrg}/pwa-microservices-template`);
490
490
  shellCd('/home/dd/engine');
491
- shellExec(`npm run update:template`);
491
+ shellExec(`npm run build:template`);
492
492
  shellExec(`cd ../pwa-microservices-template && npm install && npm run build`);
493
493
  shellCd('/home/dd/pwa-microservices-template');
494
494
  shellExec(`git add .`);
@@ -520,7 +520,7 @@ class UnderpostRelease {
520
520
  shellExec(
521
521
  `node bin secret underpost --create-from-file /home/dd/engine/engine-private/conf/dd-cron/.env.production`,
522
522
  );
523
- shellExec(`node bin/build dd conf`);
523
+ shellExec(`node bin/build dd --conf`);
524
524
  shellExec(`git add . && cd ./engine-private && git add .`);
525
525
  shellExec(`node bin cmt . ci package-pwa-microservices-template 'New release v:${version}'`);
526
526
  shellExec(`node bin cmt ./engine-private ci package-pwa-microservices-template`);