underpost 3.2.14 → 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.
package/src/cli/deploy.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  } from '../server/conf.js';
19
19
  import { loggerFactory } from '../server/logger.js';
20
20
  import { shellExec } from '../server/process.js';
21
+ import { INTERNAL_READY_PATH, INTERNAL_HEALTH_PATH } from '../server/runtime-status.js';
21
22
  import fs from 'fs-extra';
22
23
  import dotenv from 'dotenv';
23
24
  import os from 'node:os';
@@ -113,6 +114,64 @@ class UnderpostDeploy {
113
114
  )
114
115
  .join('')}`;
115
116
  },
117
+ /**
118
+ * Builds Kubernetes probes that gate on the in-pod internal status endpoint.
119
+ *
120
+ * HTTP mode (default) aligns Kubernetes pod readiness with actual Underpost
121
+ * runtime readiness:
122
+ * - readinessProbe → GET /_internal/ready (200 only when running-deployment)
123
+ * - livenessProbe → GET /_internal/health (deadlock / hung-process detection)
124
+ * - startupProbe → GET /_internal/ready (long window for hot-built/slow boots)
125
+ *
126
+ * Migration: pass `useHttp: false` to emit the legacy TCP socket probes
127
+ * (port-bound only) for deployments not yet serving the internal endpoint.
128
+ *
129
+ * @param {object} opts
130
+ * @param {number} opts.port - In-pod internal status port (deployment base PORT).
131
+ * @param {boolean} [opts.useHttp=true] - Emit HTTP probes; false → legacy TCP.
132
+ * @param {boolean} [opts.liveness=true] - Include a livenessProbe.
133
+ * @param {boolean} [opts.startup=true] - Include a startupProbe.
134
+ * @returns {{readinessProbe: object, livenessProbe?: object, startupProbe?: object}}
135
+ * @memberof UnderpostDeploy
136
+ */
137
+ runtimeProbesFactory({ port, useHttp = true, liveness = true, startup = true } = {}) {
138
+ if (!port) return {};
139
+ if (!useHttp) {
140
+ const tcp = { tcpSocket: { port }, initialDelaySeconds: 5, periodSeconds: 10, failureThreshold: 6 };
141
+ const probes = { readinessProbe: tcp };
142
+ if (liveness) probes.livenessProbe = { ...tcp, initialDelaySeconds: 30 };
143
+ return probes;
144
+ }
145
+ const probes = {
146
+ readinessProbe: {
147
+ httpGet: { path: INTERNAL_READY_PATH, port },
148
+ initialDelaySeconds: 5,
149
+ periodSeconds: 5,
150
+ timeoutSeconds: 3,
151
+ failureThreshold: 3,
152
+ },
153
+ };
154
+ if (liveness)
155
+ probes.livenessProbe = {
156
+ httpGet: { path: INTERNAL_HEALTH_PATH, port },
157
+ initialDelaySeconds: 30,
158
+ periodSeconds: 15,
159
+ timeoutSeconds: 3,
160
+ failureThreshold: 3,
161
+ };
162
+ if (startup)
163
+ // A startupProbe suspends readiness/liveness until it first succeeds, so
164
+ // its window bounds in-container hot builds and slow boots. 180 × 10s =
165
+ // 30 min before the pod is considered failed to start.
166
+ probes.startupProbe = {
167
+ httpGet: { path: INTERNAL_READY_PATH, port },
168
+ initialDelaySeconds: 10,
169
+ periodSeconds: 10,
170
+ timeoutSeconds: 3,
171
+ failureThreshold: 180,
172
+ };
173
+ return probes;
174
+ },
116
175
  /**
117
176
  * Creates a YAML deployment configuration for a deployment.
118
177
  * @param {string} deployId - Deployment ID for which the deployment is being created.
@@ -127,6 +186,11 @@ class UnderpostDeploy {
127
186
  * @param {boolean} skipFullBuild - Whether to skip the full client bundle build during deployment.
128
187
  * @param {boolean} pullBundle - Whether to pull the pre-built client bundle from Cloudinary before starting. Use together with skipFullBuild to skip the local build entirely.
129
188
  * @param {string} [imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`). When omitted, defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
189
+ * @param {object} lifecycle - Kubernetes lifecycle hooks configuration for the deployment container.
190
+ * @param {object} readinessProbe - Kubernetes readiness probe configuration for the deployment container.
191
+ * @param {object} livenessProbe - Kubernetes liveness probe configuration for the deployment container.
192
+ * @param {object} startupProbe - Kubernetes startup probe configuration for the deployment container.
193
+ * @param {number} containerPort - Container port to expose for the deployment.
130
194
  * @returns {string} - YAML deployment configuration for the specified deployment.
131
195
  * @memberof UnderpostDeploy
132
196
  */
@@ -152,7 +216,12 @@ class UnderpostDeploy {
152
216
  lifecycle,
153
217
  readinessProbe,
154
218
  livenessProbe,
219
+ startupProbe,
155
220
  containerPort,
221
+ // Explicit, secret-free internal status port injected as an env var so the
222
+ // in-pod endpoint binds exactly what the probes and the monitor target,
223
+ // independent of the ambient `PORT` baked into the image/secret.
224
+ internalStatusPort,
156
225
  }) {
157
226
  if (!cmd)
158
227
  cmd =
@@ -204,12 +273,19 @@ spec:
204
273
  - secretRef:
205
274
  name: underpost-config
206
275
  ${
207
- containerPort
208
- ? ` ports:
209
- - containerPort: ${containerPort}
276
+ internalStatusPort
277
+ ? ` env:
278
+ - name: UNDERPOST_INTERNAL_PORT
279
+ value: "${internalStatusPort}"
210
280
  `
211
281
  : ''
212
282
  }${
283
+ containerPort
284
+ ? ` ports:
285
+ - containerPort: ${containerPort}
286
+ `
287
+ : ''
288
+ }${
213
289
  resources
214
290
  ? ` resources:
215
291
  requests:
@@ -241,6 +317,15 @@ ${JSON.stringify(livenessProbe, null, 2)
241
317
  .split('\n')
242
318
  .map((l) => ' ' + l)
243
319
  .join('\n')}
320
+ `
321
+ : ''
322
+ }${
323
+ startupProbe
324
+ ? ` startupProbe:
325
+ ${JSON.stringify(startupProbe, null, 2)
326
+ .split('\n')
327
+ .map((l) => ' ' + l)
328
+ .join('\n')}
244
329
  `
245
330
  : ''
246
331
  }${
@@ -282,18 +367,22 @@ spec:
282
367
  * @param {object} options - Options for the manifest build process.
283
368
  * @param {string} options.replicas - Number of replicas for each deployment.
284
369
  * @param {string} options.image - Docker image for the deployment.
285
- * @param {string} options.namespace - Kubernetes namespace for the deployment.
370
+ * @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
286
371
  * @param {string} [options.versions] - Comma-separated list of versions to deploy.
287
372
  * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
288
- * @param {string} [options.timeoutResponse] - Timeout response setting for the deployment.
289
- * @param {string} [options.timeoutIdle] - Timeout idle setting for the deployment.
290
- * @param {string} [options.retryCount] - Retry count setting for the deployment.
291
- * @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
292
- * @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy.
293
- * @param {string} [options.traffic] - Traffic status for the deployment.
294
- * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory to generate a pull-bundle startup command.
373
+ * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
374
+ * @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
375
+ * @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
376
+ * @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
377
+ * @param {boolean} [options.disableDeploymentProxy] - Whether to disable deployment proxy route generation.
378
+ * @param {string} [options.traffic] - Comma-separated active traffic colour(s) used to select which versions receive traffic (e.g. "blue", "green").
379
+ * @param {boolean} [options.cert] - Whether to include cert-manager Certificate resources in secret.yaml (production only).
380
+ * @param {boolean} [options.selfSigned] - Whether to include TLS block in HTTPProxy using a pre-created self-signed secret. Enables HTTPS for development without cert-manager.
381
+ * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; forwarded to deploymentYamlPartsFactory.
295
382
  * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; forwarded to deploymentYamlPartsFactory. Use together with skipFullBuild.
296
- * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. When omitted, the builder defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
383
+ * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); forwarded to deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
384
+ * @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
385
+ * @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes (migration path).
297
386
  * @returns {Promise<void>} - Promise that resolves when the manifest is built.
298
387
  * @memberof UnderpostDeploy
299
388
  */
@@ -318,6 +407,17 @@ spec:
318
407
 
319
408
  logger.info('port range', { deployId, fromPort, toPort });
320
409
 
410
+ // The internal status endpoint binds `fromPort - 1`: app instances bind
411
+ // the router range starting at `fromPort`, so this slot is always free
412
+ // inside the pod. It is injected into the pod env (UNDERPOST_INTERNAL_PORT)
413
+ // and used for both the probes and the monitor's port-forward target so
414
+ // all three agree regardless of the image's ambient PORT.
415
+ // Opt out with `--disable-runtime-probes` to keep legacy probe-less pods.
416
+ const internalPort = fromPort - 1;
417
+ const probes = options.disableRuntimeProbes
418
+ ? {}
419
+ : Underpost.deploy.runtimeProbesFactory({ port: internalPort, useHttp: !options.tcpProbes });
420
+
321
421
  let deploymentYamlParts = '';
322
422
  for (const deploymentVersion of deploymentVersions) {
323
423
  deploymentYamlParts += `---
@@ -333,6 +433,10 @@ ${Underpost.deploy
333
433
  skipFullBuild: options.skipFullBuild,
334
434
  pullBundle: options.pullBundle,
335
435
  imagePullPolicy: options.imagePullPolicy,
436
+ internalStatusPort: options.disableRuntimeProbes ? undefined : internalPort,
437
+ readinessProbe: probes.readinessProbe,
438
+ livenessProbe: probes.livenessProbe,
439
+ startupProbe: probes.startupProbe,
336
440
  })
337
441
  .replace('{{ports}}', buildKindPorts(fromPort, toPort))}
338
442
  `;
@@ -375,7 +479,7 @@ ${Underpost.deploy
375
479
  : [];
376
480
 
377
481
  for (const host of Object.keys(confServer)) {
378
- if (env === 'production')
482
+ if (env === 'production' && options.cert === true)
379
483
  secretYaml += Underpost.deploy.buildCertManagerCertificate({ host, namespace: options.namespace });
380
484
 
381
485
  const pathPortAssignment = pathPortAssignmentData[host];
@@ -578,6 +682,7 @@ spec:
578
682
  * @memberof UnderpostDeploy
579
683
  */
580
684
  baseProxyYamlFactory({ host, env, options }) {
685
+ const includeTls = env !== 'development' || options.selfSigned === true;
581
686
  return `
582
687
  ---
583
688
  apiVersion: projectcontour.io/v1
@@ -588,11 +693,11 @@ metadata:
588
693
  spec:
589
694
  virtualhost:
590
695
  fqdn: ${host}${
591
- env === 'development'
592
- ? ''
593
- : `
696
+ includeTls
697
+ ? `
594
698
  tls:
595
699
  secretName: ${host}`
700
+ : ''
596
701
  }
597
702
  routes:`;
598
703
  },
@@ -608,8 +713,9 @@ spec:
608
713
  * @param {boolean} options.buildManifest - Whether to build the deployment manifest.
609
714
  * @param {boolean} options.infoUtil - Whether to display utility information.
610
715
  * @param {boolean} options.expose - Whether to expose the deployment.
611
- * @param {boolean} options.cert - Whether to create certificates for the deployment.
612
- * @param {string} options.certHosts - Comma-separated list of hosts for which to create certificates.
716
+ * @param {boolean} options.cert - Whether to create cert-manager Certificate resources for the deployment.
717
+ * @param {string} options.certHosts - Comma-separated list of hosts for which to create cert-manager certificates.
718
+ * @param {boolean} options.selfSigned - Use a pre-created self-signed TLS secret instead of cert-manager. The secret must already exist in the namespace with the same name as the host. Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.
613
719
  * @param {string} options.versions - Comma-separated list of versions to deploy.
614
720
  * @param {string} options.image - Docker image for the deployment.
615
721
  * @param {string} options.traffic - Traffic status for the deployment.
@@ -621,22 +727,27 @@ spec:
621
727
  * @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
622
728
  * @param {boolean} options.status - Whether to display deployment status.
623
729
  * @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
624
- * @param {string} [options.namespace] - Kubernetes namespace for the deployment.
625
- * @param {string} [options.timeoutResponse] - Timeout response setting for the deployment.
626
- * @param {string} [options.timeoutIdle] - Timeout idle setting for the deployment.
627
- * @param {string} [options.retryCount] - Retry count setting for the deployment.
628
- * @param {string} [options.retryPerTryTimeout] - Retry per-try timeout setting for the deployment.
629
- * @param {string} [options.kindType] - Type of Kubernetes resource to retrieve information for.
630
- * @param {number} [options.port] - Port number for exposing the deployment.
631
- * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
632
- * @param {number} [options.exposePort] - Local:remote port override when --expose is active (overrides auto-detected service port).
730
+ * @param {string} [options.namespace] - Kubernetes namespace for the deployment (defaults to "default").
731
+ * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
732
+ * @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
733
+ * @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
734
+ * @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
735
+ * @param {string} [options.kindType] - Kubernetes resource kind to target when using --expose (defaults to "svc").
736
+ * @param {number} [options.port] - Port number override for exposing the deployment.
737
+ * @param {string} [options.cmd] - Custom initialization command (comma-separated) for deploymentYamlPartsFactory.
738
+ * @param {number} [options.exposePort] - Remote port override when --expose is active (overrides auto-detected service port). Used as both local and remote port unless exposeLocalPort is also set.
739
+ * @param {number} [options.exposeLocalPort] - Local port override for --expose (e.g. 80); remote port is still auto-detected. Enables /etc/hosts access without a port in the browser URL.
740
+ * @param {boolean} [options.localProxy] - When true (with --expose), forward all service TCP ports locally and start the Node.js path-routing proxy for full path-based routing (e.g. /wp alongside /).
741
+ * @param {boolean} [options.tls] - When true (with --expose --local-proxy), start the proxy on port 443 with TLS using self-signed certificates resolved from the local SSL store.
633
742
  * @param {boolean} [options.k3s] - Whether to use k3s cluster context.
634
743
  * @param {boolean} [options.kubeadm] - Whether to use kubeadm cluster context.
635
744
  * @param {boolean} [options.kind] - Whether to use kind cluster context.
636
745
  * @param {boolean} [options.gitClean] - Whether to run git clean on volume mount paths before copying.
637
746
  * @param {boolean} [options.skipFullBuild] - Whether to skip the full client bundle build; passed through to buildManifest/deploymentYamlPartsFactory.
638
747
  * @param {boolean} [options.pullBundle] - Whether to pull the pre-built client bundle from Cloudinary; passed through to buildManifest/deploymentYamlPartsFactory. Use together with skipFullBuild.
639
- * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory. When omitted, the builder defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
748
+ * @param {string} [options.imagePullPolicy] - Container imagePullPolicy override (`Always`, `IfNotPresent`, `Never`); passed through to buildManifest/deploymentYamlPartsFactory. Defaults to `Never` for `localhost/` images and `IfNotPresent` otherwise.
749
+ * @param {boolean} [options.disableRuntimeProbes] - Omit internal-status HTTP probes from generated manifests. When true no readiness/liveness/startup probes are emitted.
750
+ * @param {boolean} [options.tcpProbes] - Emit legacy TCP socket probes instead of HTTP internal-status probes.
640
751
  * @returns {Promise<void>} - Promise that resolves when the deployment process is complete.
641
752
  * @memberof UnderpostDeploy
642
753
  */
@@ -671,6 +782,10 @@ spec:
671
782
  kindType: '',
672
783
  port: 0,
673
784
  exposePort: 0,
785
+ exposeLocalPort: 0,
786
+ localProxy: false,
787
+ tls: false,
788
+ selfSigned: false,
674
789
  cmd: '',
675
790
  k3s: false,
676
791
  kubeadm: false,
@@ -756,20 +871,50 @@ EOF`);
756
871
  logger.error(`No ${kindType} found matching '${deployId}', skipping expose`);
757
872
  continue;
758
873
  }
759
- const port = options.exposePort
760
- ? parseInt(options.exposePort)
761
- : options.port
762
- ? parseInt(options.port)
763
- : kindType !== 'svc'
764
- ? 80
765
- : parseInt(svc[`PORT(S)`].split('/TCP')[0]);
766
- logger.info(deployId, {
767
- svc,
768
- port,
769
- });
770
- shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${port}:${port}`, {
771
- async: true,
772
- });
874
+ if (options.localProxy) {
875
+ const svcPorts = [
876
+ ...new Set(
877
+ svc['PORT(S)']
878
+ .split(',')
879
+ .filter((p) => p.includes('/TCP'))
880
+ .map((p) => parseInt(p.split(':')[0])),
881
+ ),
882
+ ];
883
+ for (const svcPort of svcPorts) {
884
+ shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${svcPort}:${svcPort}`, {
885
+ async: true,
886
+ });
887
+ }
888
+ const envFile = `./engine-private/conf/${deployId}/.env.${env}`;
889
+ let basePort = svcPorts[0] - 1;
890
+ if (fs.existsSync(envFile)) {
891
+ const portMatch = fs.readFileSync(envFile, 'utf8').match(/^PORT=(\d+)/m);
892
+ if (portMatch) basePort = parseInt(portMatch[1]);
893
+ }
894
+ logger.info(deployId, { svc, svcPorts, basePort });
895
+ const tlsFlag = options.tls ? ' tls' : '';
896
+ shellExec(
897
+ `NODE_ENV=${env} PORT=${basePort} DEV_PROXY_PORT_OFFSET=0 node src/proxy proxy ${deployId} ${env}${tlsFlag}`,
898
+ { async: true },
899
+ );
900
+ } else {
901
+ const remotePort = options.exposePort
902
+ ? parseInt(options.exposePort)
903
+ : options.port
904
+ ? parseInt(options.port)
905
+ : kindType !== 'svc'
906
+ ? 80
907
+ : parseInt(svc[`PORT(S)`].split('/TCP')[0]);
908
+ const localPort = options.exposeLocalPort ? parseInt(options.exposeLocalPort) : remotePort;
909
+ logger.info(deployId, {
910
+ svc,
911
+ localPort,
912
+ remotePort,
913
+ });
914
+ shellExec(`sudo kubectl port-forward -n ${namespace} ${kindType}/${svc.NAME} ${localPort}:${remotePort}`, {
915
+ async: true,
916
+ });
917
+ }
773
918
  continue;
774
919
  }
775
920
 
@@ -821,7 +966,10 @@ EOF`);
821
966
  if (!options.disableUpdateProxy)
822
967
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/proxy.yaml -n ${namespace}`);
823
968
 
824
- if (Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }))
969
+ if (
970
+ Underpost.deploy.isValidTLSContext({ host: Object.keys(confServer)[0], env, options }) &&
971
+ !options.selfSigned
972
+ )
825
973
  shellExec(`sudo kubectl apply -f ./${manifestsPath}/secret.yaml -n ${namespace}`);
826
974
  }
827
975
  }
@@ -1082,9 +1230,10 @@ spec:
1082
1230
  * @memberof UnderpostDeploy
1083
1231
  */
1084
1232
  isValidTLSContext: ({ host, env, options }) =>
1085
- env === 'production' &&
1086
- options.cert === true &&
1087
- (!options.certHosts || options.certHosts.split(',').includes(host)),
1233
+ (env === 'production' &&
1234
+ options.cert === true &&
1235
+ (!options.certHosts || options.certHosts.split(',').includes(host))) ||
1236
+ options.selfSigned === true,
1088
1237
 
1089
1238
  /**
1090
1239
  * Predefined resource templates for Kubernetes deployments.
package/src/cli/env.js CHANGED
@@ -95,10 +95,7 @@ class UnderpostRootEnv {
95
95
  get(key, value, options = { plain: false, disableLog: false, copy: false }) {
96
96
  const exeRootPath = `${getNpmRootPath()}/underpost`;
97
97
  const envPath = `${exeRootPath}/.env`;
98
- if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) {
99
- logger.warn(`Empty environment variables`);
100
- return undefined;
101
- }
98
+ if (!fs.existsSync(envPath) || !fs.statSync(envPath).isFile()) return undefined;
102
99
  const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
103
100
  if (!options.disableLog)
104
101
  options?.plain === true ? console.log(env[key]) : logger.info(`${key}(${typeof env[key]})`, env[key]);
package/src/cli/index.js CHANGED
@@ -124,6 +124,10 @@ program
124
124
  '--is-remote-repo <url-repo>',
125
125
  'Checks whether a remote Git repository URL is reachable. Prints true or false.',
126
126
  )
127
+ .option(
128
+ '--has-changes',
129
+ 'Prints "1" if there are staged or unstaged git changes in the repository, empty string otherwise.',
130
+ )
127
131
  .description('Manages commits to a GitHub repository, supporting various commit types and options.')
128
132
  .action(Underpost.repo.commit);
129
133
 
@@ -315,6 +319,12 @@ program
315
319
  .option('--expose', 'Exposes services matching the provided deployment ID list.')
316
320
  .option('--cert', 'Resets TLS/SSL certificate secrets for deployments.')
317
321
  .option('--cert-hosts <hosts>', 'Resets TLS/SSL certificate secrets for specified hosts.')
322
+ .option(
323
+ '--self-signed',
324
+ 'Use a pre-created self-signed TLS secret (kubernetes.io/tls) instead of cert-manager. ' +
325
+ 'The secret must already exist in the namespace with the same name as the host. ' +
326
+ 'Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.',
327
+ )
318
328
  .option('--node <node>', 'Sets optional node for deployment operations.')
319
329
  .option(
320
330
  '--build-manifest',
@@ -332,6 +342,8 @@ program
332
342
  .option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
333
343
  .option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
334
344
  .option('--disable-update-deployment', 'Disables updates to deployments.')
345
+ .option('--disable-runtime-probes', 'Omits the internal-status HTTP probes from generated deployment manifests.')
346
+ .option('--tcp-probes', 'Generates legacy TCP socket probes instead of HTTP internal-status probes (migration).')
335
347
  .option('--disable-update-proxy', 'Disables updates to proxies.')
336
348
  .option('--disable-deployment-proxy', 'Disables proxies of deployments.')
337
349
  .option('--disable-update-volume', 'Disables updates to volume mounts during deployment.')
@@ -351,6 +363,14 @@ program
351
363
  '--expose-port <port>',
352
364
  'Sets the local:remote port to expose when --expose is active (overrides auto-detected service port).',
353
365
  )
366
+ .option(
367
+ '--expose-local-port <port>',
368
+ 'Sets a different local port for --expose (e.g. 80) while keeping the remote service port. Useful for /etc/hosts local access without specifying a port in the browser.',
369
+ )
370
+ .option(
371
+ '--local-proxy',
372
+ 'Forward all service TCP ports locally and start the Node.js path-routing proxy. Enables full path-based routing (e.g. /wp alongside /) without needing --expose-local-port. Requires --expose.',
373
+ )
354
374
  .option('--cmd <cmd>', 'Custom initialization command for deployment (comma-separated commands).')
355
375
  .option(
356
376
  '--skip-full-build',
@@ -364,6 +384,12 @@ program
364
384
  '--image-pull-policy <policy>',
365
385
  'Override container imagePullPolicy in the generated deployment manifest (Always, IfNotPresent, Never). Defaults to Never for localhost/ images and IfNotPresent otherwise.',
366
386
  )
387
+ .option(
388
+ '--tls',
389
+ 'Enables TLS for the local proxy started by --expose --local-proxy. ' +
390
+ 'The proxy will serve HTTPS on port 443 using self-signed certificates resolved from the local SSL store. ' +
391
+ 'Use together with --expose and --local-proxy.',
392
+ )
367
393
  .description('Manages application deployments, defaulting to deploying development pods.')
368
394
  .action(Underpost.deploy.callback);
369
395
 
@@ -889,6 +915,19 @@ program
889
915
  '--dry-run',
890
916
  'For --build: previews version-bump changes (per-file substitution counts) without writing files or running downstream commands.',
891
917
  )
918
+ .option(
919
+ '--mongo-host <host>',
920
+ 'For --build: override DB_HOST in the template .env.example for the smoke test (e.g., "192.168.1.82:27017").',
921
+ )
922
+ .option('--mongo-user <user>', 'For --build: override DB_USER in the template .env.example for the smoke test.')
923
+ .option(
924
+ '--mongo-password <password>',
925
+ 'For --build: override DB_PASSWORD in the template .env.example for the smoke test.',
926
+ )
927
+ .option(
928
+ '--valkey-host <host>',
929
+ 'For --build: override VALKEY_HOST in the template .env.example for the smoke test (e.g., "192.168.1.82").',
930
+ )
892
931
  .description('Release orchestrator for building new versions and deploying releases of the Underpost CLI.')
893
932
  .action(async (version, options) => {
894
933
  if (options.build) return Underpost.release.build(version, options);