underpost 3.2.30 → 3.2.70
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/.github/workflows/ghpkg.ci.yml +87 -0
- package/.github/workflows/npmpkg.ci.yml +9 -6
- package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
- package/CHANGELOG.md +121 -1
- package/CLI-HELP.md +12 -6
- package/README.md +2 -2
- package/bin/build.js +30 -0
- package/bin/deploy.js +2 -2
- package/bump.config.js +1 -0
- package/docker-compose.yml +26 -26
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +10 -10
- package/scripts/disk-clean.sh +85 -60
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/cluster.js +77 -4
- package/src/cli/deploy.js +9 -2
- package/src/cli/docker-compose.js +58 -1
- package/src/cli/fs.js +45 -24
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +21 -6
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +85 -22
- package/src/cli/run.js +237 -108
- package/src/cli/secrets.js +75 -46
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +0 -3
package/src/cli/image.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import nodePath from 'path';
|
|
10
|
+
import crypto from 'crypto';
|
|
8
11
|
import { loggerFactory } from '../server/logger.js';
|
|
9
12
|
import Underpost from '../index.js';
|
|
10
|
-
import { getNpmRootPath
|
|
13
|
+
import { getNpmRootPath } from '../server/conf.js';
|
|
11
14
|
import { shellExec } from '../server/process.js';
|
|
12
15
|
|
|
13
16
|
const logger = loggerFactory(import.meta);
|
|
@@ -23,59 +26,32 @@ class UnderpostImage {
|
|
|
23
26
|
static API = {
|
|
24
27
|
/**
|
|
25
28
|
* @method pullBaseImages
|
|
26
|
-
* @description
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @param {boolean} [options.k3s=false] - If true, load image into K3s cluster.
|
|
32
|
-
* @param {string} [options.path=''] - Path to the Dockerfile context.
|
|
33
|
-
* @param {boolean} [options.dev=false] - If true, use development mode.
|
|
34
|
-
* @param {string} [options.version=''] - Version tag for the image.
|
|
35
|
-
* @param {string} [options.imageName=''] - Custom name for the image.
|
|
29
|
+
* @description Ensures the base image prerequisites for the runtime Dockerfiles
|
|
30
|
+
* are present on the host (currently `docker.io/rockylinux/rockylinux:9`). This
|
|
31
|
+
* only pulls — it does NOT build. Builds run with `podman build --pull=never`,
|
|
32
|
+
* so the base must exist locally beforehand; that is the sole purpose of this
|
|
33
|
+
* step. Combine with `--build` in the same command to pull-then-build.
|
|
36
34
|
* @memberof UnderpostImage
|
|
37
35
|
*/
|
|
38
|
-
pullBaseImages(
|
|
39
|
-
|
|
40
|
-
kind: false,
|
|
41
|
-
kubeadm: false,
|
|
42
|
-
k3s: false,
|
|
43
|
-
path: '',
|
|
44
|
-
dev: false,
|
|
45
|
-
version: '',
|
|
46
|
-
imageName: '',
|
|
47
|
-
},
|
|
48
|
-
) {
|
|
49
|
-
shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
|
|
50
|
-
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
51
|
-
const baseCommandOption = options.dev ? ' --dev' : '';
|
|
52
|
-
const IMAGE_NAME = options.imageName
|
|
53
|
-
? `options.imageName${options.version ? `:${options.version}` : ''}`
|
|
54
|
-
: `rockylinux9-underpost:${options.version ? options.version : Underpost.version}`;
|
|
55
|
-
let LOAD_TYPE = '';
|
|
56
|
-
if (options.kind === true) LOAD_TYPE = `--kind`;
|
|
57
|
-
else if (options.kubeadm === true) LOAD_TYPE = `--kubeadm`;
|
|
58
|
-
else if (options.k3s === true) LOAD_TYPE = `--k3s`;
|
|
59
|
-
shellExec(
|
|
60
|
-
`${baseCommand} image${baseCommandOption} --build --podman-save --reset --image-path=. --path ${
|
|
61
|
-
options.path ? options.path : getUnderpostRootPath()
|
|
62
|
-
} --image-name=${IMAGE_NAME} ${LOAD_TYPE}`,
|
|
63
|
-
);
|
|
36
|
+
pullBaseImages() {
|
|
37
|
+
shellExec(`sudo podman pull docker.io/rockylinux/rockylinux:9`);
|
|
64
38
|
},
|
|
65
39
|
/**
|
|
66
40
|
* @method build
|
|
67
41
|
* @description Builds a Docker image using Podman, optionally saves it as a tar archive,
|
|
68
|
-
* and loads it into a specified
|
|
42
|
+
* and loads it into a specified target (Kind, Kubeadm, or K3s cluster, or the local
|
|
43
|
+
* Docker store for Docker Compose).
|
|
69
44
|
* @param {object} options - Options for building and loading images.
|
|
70
45
|
* @param {string} [options.path=''] - The path to the directory containing the Dockerfile.
|
|
71
46
|
* @param {string} [options.imageName=''] - The name and tag for the image (e.g., 'my-app:latest').
|
|
72
47
|
* @param {string} [options.version=''] - Version tag for the image.
|
|
73
|
-
* @param {string} [options.
|
|
48
|
+
* @param {string} [options.imageOutPath=''] - Directory to save the image tar file.
|
|
74
49
|
* @param {string} [options.dockerfileName=''] - Name of the Dockerfile (defaults to 'Dockerfile').
|
|
75
50
|
* @param {boolean} [options.podmanSave=false] - If true, save the image as a tar archive using Podman.
|
|
76
51
|
* @param {boolean} [options.kind=false] - If true, load the image archive into a Kind cluster.
|
|
77
52
|
* @param {boolean} [options.kubeadm=false] - If true, load the image archive into a Kubeadm cluster (uses 'ctr').
|
|
78
53
|
* @param {boolean} [options.k3s=false] - If true, load the image archive into a K3s cluster (uses 'k3s ctr').
|
|
54
|
+
* @param {boolean} [options.dockerCompose=false] - If true, load the image archive into the local Docker store for Docker Compose.
|
|
79
55
|
* @param {boolean} [options.reset=false] - If true, perform a no-cache build.
|
|
80
56
|
* @param {boolean} [options.dev=false] - If true, use development mode.
|
|
81
57
|
* @memberof UnderpostImage
|
|
@@ -85,42 +61,144 @@ class UnderpostImage {
|
|
|
85
61
|
path: '',
|
|
86
62
|
imageName: '',
|
|
87
63
|
version: '',
|
|
88
|
-
|
|
64
|
+
imageOutPath: '',
|
|
89
65
|
dockerfileName: '',
|
|
90
66
|
podmanSave: false,
|
|
91
67
|
kind: false,
|
|
92
68
|
kubeadm: false,
|
|
93
69
|
k3s: false,
|
|
70
|
+
dockerCompose: false,
|
|
94
71
|
reset: false,
|
|
95
72
|
dev: false,
|
|
96
73
|
},
|
|
97
74
|
) {
|
|
98
|
-
let {
|
|
75
|
+
let {
|
|
76
|
+
path,
|
|
77
|
+
imageName,
|
|
78
|
+
version,
|
|
79
|
+
imageOutPath,
|
|
80
|
+
dockerfileName,
|
|
81
|
+
podmanSave,
|
|
82
|
+
kind,
|
|
83
|
+
kubeadm,
|
|
84
|
+
k3s,
|
|
85
|
+
dockerCompose,
|
|
86
|
+
reset,
|
|
87
|
+
dev,
|
|
88
|
+
} = options;
|
|
99
89
|
if (!path) path = '.';
|
|
100
90
|
if (!imageName) imageName = `rockylinux9-underpost:${Underpost.version}`;
|
|
101
|
-
if (!
|
|
91
|
+
if (!imageOutPath) imageOutPath = '.';
|
|
102
92
|
if (imageName.match('/')) imageName = imageName.split('/')[1];
|
|
103
93
|
if (!version) version = 'latest';
|
|
104
94
|
version = imageName && imageName.match(':') ? '' : `:${version}`;
|
|
105
95
|
const podManImg = `localhost/${imageName}${version}`;
|
|
106
|
-
if (
|
|
107
|
-
fs.mkdirSync(
|
|
108
|
-
const tarFile = `${
|
|
96
|
+
if (imageOutPath && typeof imageOutPath === 'string' && !fs.existsSync(imageOutPath))
|
|
97
|
+
fs.mkdirSync(imageOutPath, { recursive: true });
|
|
98
|
+
const tarFile = `${imageOutPath}/${imageName.replace(':', '_')}.tar`;
|
|
109
99
|
let cache = '';
|
|
110
100
|
if (reset === true) cache += ' --rm --no-cache';
|
|
101
|
+
|
|
102
|
+
// Forward GitHub credentials from the host environment into the build as
|
|
103
|
+
// podman/BuildKit secrets (`--secret id=...,src=...`), matching the
|
|
104
|
+
// `RUN --mount=type=secret,id=github_*` contract in the runtime
|
|
105
|
+
// Dockerfiles (e.g. src/runtime/engine-cyberia). Secrets are written to
|
|
106
|
+
// short-lived 0600 temp files and removed right after the build — they are
|
|
107
|
+
// never passed as build-args (which would persist in image history) nor
|
|
108
|
+
// baked into any layer.
|
|
109
|
+
const secretTmpFiles = [];
|
|
110
|
+
const secretFlags = [];
|
|
111
|
+
const addBuildSecret = (id, value) => {
|
|
112
|
+
if (!value) return;
|
|
113
|
+
const file = nodePath.join(os.tmpdir(), `underpost-secret-${id}-${crypto.randomBytes(6).toString('hex')}`);
|
|
114
|
+
fs.writeFileSync(file, String(value), { mode: 0o600 });
|
|
115
|
+
secretTmpFiles.push(file);
|
|
116
|
+
secretFlags.push(`--secret id=${id},src=${file}`);
|
|
117
|
+
};
|
|
118
|
+
// addBuildSecret('github_token', process.env.GITHUB_TOKEN);
|
|
119
|
+
addBuildSecret('github_username', process.env.GITHUB_USERNAME);
|
|
120
|
+
// Cloudinary creds power build-time asset pulls (`node bin fs --pull`).
|
|
121
|
+
// addBuildSecret('cloudinary_cloud_name', process.env.CLOUDINARY_CLOUD_NAME);
|
|
122
|
+
// addBuildSecret('cloudinary_api_key', process.env.CLOUDINARY_API_KEY);
|
|
123
|
+
// addBuildSecret('cloudinary_api_secret', process.env.CLOUDINARY_API_SECRET);
|
|
124
|
+
const secretArgs = secretFlags.length ? ` ${secretFlags.join(' ')}` : '';
|
|
125
|
+
if (secretFlags.length)
|
|
126
|
+
logger.info('Passing host GitHub credentials as build secrets', { ids: secretFlags.length });
|
|
127
|
+
|
|
111
128
|
if (path)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
try {
|
|
130
|
+
shellExec(
|
|
131
|
+
`cd ${path} && sudo podman build -f ./${
|
|
132
|
+
dockerfileName && typeof dockerfileName === 'string' ? dockerfileName : 'Dockerfile'
|
|
133
|
+
} -t ${imageName} --pull=never --cap-add=CAP_AUDIT_WRITE${cache}${secretArgs} --network host`,
|
|
134
|
+
);
|
|
135
|
+
} finally {
|
|
136
|
+
for (const file of secretTmpFiles) {
|
|
137
|
+
try {
|
|
138
|
+
fs.removeSync(file);
|
|
139
|
+
} catch {
|
|
140
|
+
/* best-effort cleanup */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Loading into any target requires the tar archive, so imply the save when
|
|
145
|
+
// one is set (kind/kubeadm/k3s/docker-compose) even if --podman-save was omitted.
|
|
146
|
+
const loadTarget = kind === true || kubeadm === true || k3s === true || dockerCompose === true;
|
|
147
|
+
if (podmanSave === true || loadTarget) {
|
|
118
148
|
if (fs.existsSync(tarFile)) fs.removeSync(tarFile);
|
|
119
149
|
shellExec(`podman save -o ${tarFile} ${podManImg}`);
|
|
120
150
|
}
|
|
121
151
|
if (kind === true) shellExec(`sudo kind load image-archive ${tarFile}`);
|
|
122
152
|
else if (kubeadm === true) shellExec(`sudo ctr -n k8s.io images import ${tarFile}`);
|
|
123
153
|
else if (k3s === true) shellExec(`sudo k3s ctr images import ${tarFile}`);
|
|
154
|
+
// Independent of any cluster target: make the local image available to the
|
|
155
|
+
// Docker daemon so `docker compose` can resolve it (e.g. ENGINE_CYBERIA_IMAGE).
|
|
156
|
+
if (dockerCompose === true) shellExec(`sudo docker load -i ${tarFile}`);
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* @method importTar
|
|
160
|
+
* @description Loads a pre-built image tar archive into each enabled target
|
|
161
|
+
* without building anything. Mirrors the load step of {@link build}, but
|
|
162
|
+
* the archive is supplied directly via `--import-tar <tar-path>` and every
|
|
163
|
+
* enabled target flag is honored (the same archive is loaded into each), so
|
|
164
|
+
* `--kind --docker-compose` loads it into both.
|
|
165
|
+
* @param {object} options - CLI options.
|
|
166
|
+
* @param {string} options.importTar - Path to the image tar archive (e.g. `./image-v1.0.0.tar`).
|
|
167
|
+
* @param {boolean} [options.kind] - Load into the Kind cluster (`kind load image-archive`).
|
|
168
|
+
* @param {boolean} [options.kubeadm] - Import into kubeadm containerd (`ctr -n k8s.io images import`).
|
|
169
|
+
* @param {boolean} [options.k3s] - Import into k3s containerd (`k3s ctr images import`).
|
|
170
|
+
* @param {boolean} [options.dockerCompose] - Load into the local Docker daemon (`docker load`) for Docker Compose.
|
|
171
|
+
* @returns {void}
|
|
172
|
+
* @memberof UnderpostImage
|
|
173
|
+
*/
|
|
174
|
+
importTar(options = { importTar: '', kind: false, kubeadm: false, k3s: false, dockerCompose: false }) {
|
|
175
|
+
const { importTar, kind, kubeadm, k3s, dockerCompose } = options;
|
|
176
|
+
if (!importTar || typeof importTar !== 'string' || !fs.existsSync(importTar)) {
|
|
177
|
+
logger.error('image --import-tar: archive not found', { importTar });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const targets = [];
|
|
181
|
+
if (kind === true) {
|
|
182
|
+
shellExec(`sudo kind load image-archive ${importTar}`);
|
|
183
|
+
targets.push('kind');
|
|
184
|
+
}
|
|
185
|
+
if (kubeadm === true) {
|
|
186
|
+
shellExec(`sudo ctr -n k8s.io images import ${importTar}`);
|
|
187
|
+
targets.push('kubeadm');
|
|
188
|
+
}
|
|
189
|
+
if (k3s === true) {
|
|
190
|
+
shellExec(`sudo k3s ctr images import ${importTar}`);
|
|
191
|
+
targets.push('k3s');
|
|
192
|
+
}
|
|
193
|
+
if (dockerCompose === true) {
|
|
194
|
+
shellExec(`sudo docker load -i ${importTar}`);
|
|
195
|
+
targets.push('docker-compose');
|
|
196
|
+
}
|
|
197
|
+
if (targets.length === 0)
|
|
198
|
+
logger.warn(
|
|
199
|
+
'image --import-tar: no target enabled; combine with --kind, --kubeadm, --k3s and/or --docker-compose',
|
|
200
|
+
);
|
|
201
|
+
else logger.info('image --import-tar: archive loaded', { importTar, targets });
|
|
124
202
|
},
|
|
125
203
|
/**
|
|
126
204
|
* @method getCurrentLoaded
|
package/src/cli/index.js
CHANGED
|
@@ -110,16 +110,18 @@ program
|
|
|
110
110
|
.option('--cached', 'Commit staged changes only or context.')
|
|
111
111
|
.option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
|
|
112
112
|
.option('--extension <extension>', 'specific file extensions of commits.')
|
|
113
|
-
.option(
|
|
114
|
-
'--changelog [latest-n]',
|
|
115
|
-
'Print plain the changelog of the specified number of latest n commits, if no number is provided it will get the changelog to latest ci integration',
|
|
116
|
-
)
|
|
113
|
+
.option('--changelog', 'Print the plain changelog of the last N commits (see --from-n-commit, default 1).')
|
|
117
114
|
.option('--changelog-build', 'Builds a CHANGELOG.md file based on the commit history')
|
|
118
115
|
.option('--changelog-min-version <version>', 'Sets the minimum version limit for --changelog-build (default: 2.85.0)')
|
|
119
116
|
.option(
|
|
120
117
|
'--changelog-no-hash',
|
|
121
118
|
'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
|
|
122
119
|
)
|
|
120
|
+
.option(
|
|
121
|
+
'--changelog-msg',
|
|
122
|
+
'Print the sanitized, commit-ready changelog message of the last N commits (see --from-n-commit, default 1). Empty when there are no tagged entries.',
|
|
123
|
+
)
|
|
124
|
+
.option('--from-n-commit <n>', 'Number of latest commits to include in --changelog/--changelog-msg (default: 1).')
|
|
123
125
|
.option('--unpush', 'With --log, automatically sets range to unpushed commits ahead of remote.')
|
|
124
126
|
.option('-b', 'Shows the current Git branch name.')
|
|
125
127
|
.option('-p [branch]', 'Shows the reflog for the specified branch.')
|
|
@@ -434,25 +436,31 @@ program
|
|
|
434
436
|
.option('--rm <image-id>', 'Removes specified Underpost Dockerfile images.')
|
|
435
437
|
.option('--path [path]', 'The path to the Dockerfile directory.')
|
|
436
438
|
.option('--image-name [image-name]', 'Sets a custom name for the Docker image.')
|
|
437
|
-
.option('--image-path [image-path]', 'Sets the output path for the tar image archive.')
|
|
439
|
+
.option('--image-out-path [image-out-path]', 'Sets the output path for the tar image archive.')
|
|
438
440
|
.option('--dockerfile-name [dockerfile-name]', 'Sets a custom name for the Dockerfile.')
|
|
439
441
|
.option('--podman-save', 'Exports the built image as a tar file using Podman.')
|
|
440
|
-
.option('--pull-base', 'Pulls base
|
|
442
|
+
.option('--pull-base', 'Pulls the base image prerequisites (rockylinux:9) on the host; combine with --build.')
|
|
441
443
|
.option('--spec', 'Get current cached list of container images used by all pods')
|
|
442
444
|
.option('--namespace <namespace>', 'Kubernetes namespace for image operations (defaults to "default").')
|
|
443
445
|
.option('--kind', 'Set kind cluster env image context management.')
|
|
444
446
|
.option('--kubeadm', 'Set kubeadm cluster env image context management.')
|
|
445
447
|
.option('--k3s', 'Set k3s cluster env image context management.')
|
|
448
|
+
.option('--docker-compose', 'Load the built image tar into the local Docker store for Docker Compose availability.')
|
|
446
449
|
.option('--node-name', 'Set node name for kubeadm or k3s cluster env image context management.')
|
|
447
450
|
.option('--reset', 'Performs a build without using the cache.')
|
|
448
451
|
.option('--dev', 'Use development mode.')
|
|
449
452
|
.option('--pull-dockerhub <dockerhub-image>', 'Sets a custom Docker Hub image for base image pulls.')
|
|
453
|
+
.option(
|
|
454
|
+
'--import-tar <tar-path>',
|
|
455
|
+
'Load a pre-built image tar archive (e.g. ./image-v1.0.0.tar) into the enabled target(s) without building. Combine with --kind, --kubeadm, --k3s and/or --docker-compose; the archive is loaded into each enabled one.',
|
|
456
|
+
)
|
|
450
457
|
.description('Manages Docker images, including building, saving, and loading into Kubernetes clusters.')
|
|
451
458
|
.action(async (options) => {
|
|
452
459
|
if (options.rm) Underpost.image.rm({ ...options, imageName: options.rm });
|
|
453
460
|
if (options.ls) Underpost.image.list({ ...options, log: true });
|
|
454
461
|
if (options.pullBase) Underpost.image.pullBaseImages(options);
|
|
455
462
|
if (options.build) Underpost.image.build(options);
|
|
463
|
+
if (options.importTar) Underpost.image.importTar(options);
|
|
456
464
|
if (options.pullDockerhub)
|
|
457
465
|
Underpost.image.pullDockerHubImage({ ...options, dockerhubImage: options.pullDockerhub });
|
|
458
466
|
});
|
|
@@ -692,6 +700,7 @@ program
|
|
|
692
700
|
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
|
|
693
701
|
.option('--k3s', 'Sets the k3s cluster context for the runner execution.')
|
|
694
702
|
.option('--kind', 'Sets the kind cluster context for the runner execution.')
|
|
703
|
+
.option('--traffic <traffic>', 'Blue/green traffic colour to bake into generated manifests (default: blue).')
|
|
695
704
|
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
|
|
696
705
|
.option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
|
|
697
706
|
.option('--user <user>', 'Sets user context for the runner execution.')
|
|
@@ -765,6 +774,12 @@ program
|
|
|
765
774
|
'--deploy-id <deploy-id>',
|
|
766
775
|
"Deployment to run as the app container (default: dd-default). 'dd-default' self-bootstraps a fresh engine; any other id runs the standard 'underpost start' command (mirrors src/cli/deploy.js).",
|
|
767
776
|
)
|
|
777
|
+
.option(
|
|
778
|
+
'--docker-compose-id <docker-compose-id>',
|
|
779
|
+
'Selects a canonical custom-workflow stack at engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/ ' +
|
|
780
|
+
'(docker-compose.yml + compose.env + nginx.conf, used as-is; nginx/env generation is skipped). ' +
|
|
781
|
+
'e.g. --deploy-id dd-cyberia --docker-compose-id cyberia for the Cyberia MMO ecosystem.',
|
|
782
|
+
)
|
|
768
783
|
.option('--env <env>', 'Deployment environment for non-default deploy ids (default: development).')
|
|
769
784
|
.option('--generate', 'Render dynamic supporting files (nginx router config, env-file, app-command override).')
|
|
770
785
|
.option('--up', 'Start the full stack detached (regenerates config first).')
|
package/src/cli/release.js
CHANGED
|
@@ -117,9 +117,53 @@ const buildVersionBumpTargets = () => [
|
|
|
117
117
|
/(underpost-engine:v)\d+\.\d+\.\d+/g,
|
|
118
118
|
/(type=raw,value=v)\d+\.\d+\.\d+/g,
|
|
119
119
|
/(UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
|
|
120
|
+
/(UNDERPOST_VERSION:\s*['"]?)\d+\.\d+\.\d+/g,
|
|
120
121
|
],
|
|
121
122
|
},
|
|
122
123
|
|
|
124
|
+
// ── Docker-compose image-tag defaults. Root compose + generator + cyberia runtime compose.
|
|
125
|
+
// Tags live in `${VAR:-vX.Y.Z}` / `VAR=vX.Y.Z` shapes bumpp cannot detect. ──
|
|
126
|
+
{
|
|
127
|
+
file: 'docker-compose.yml',
|
|
128
|
+
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
file: 'src/cli/docker-compose.js',
|
|
132
|
+
patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
file: 'src/runtime/engine-cyberia/docker-compose.yml',
|
|
136
|
+
patterns: /(_TAG:-v)\d+\.\d+\.\d+/g,
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ── Cyberia CLI dev image tar/name defaults (bin/cyberia.js). ──
|
|
140
|
+
{
|
|
141
|
+
file: 'bin/cyberia.js',
|
|
142
|
+
patterns: [/(-dev_v)\d+\.\d+\.\d+(?=\.tar)/g, /(-dev:v)\d+\.\d+\.\d+/g],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ── Runtime Dockerfiles: `ARG UNDERPOST_VERSION=X.Y.Z` build-arg defaults. ──
|
|
146
|
+
{
|
|
147
|
+
dir: 'src/runtime',
|
|
148
|
+
match: /^Dockerfile(\.\w+)?$/,
|
|
149
|
+
patterns: /(ARG UNDERPOST_VERSION=)\d+\.\d+\.\d+/g,
|
|
150
|
+
recursive: true,
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// ── Runtime compose.env shipped tag defaults ──
|
|
154
|
+
{
|
|
155
|
+
dir: 'src/runtime',
|
|
156
|
+
match: /^compose\.env$/,
|
|
157
|
+
patterns: /(_TAG=v)\d+\.\d+\.\d+/g,
|
|
158
|
+
recursive: true,
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── Deploy-monitor smoke-test image default (scripts/test-monitor.sh). ──
|
|
162
|
+
{
|
|
163
|
+
file: 'scripts/test-monitor.sh',
|
|
164
|
+
patterns: /(underpost\/[a-z0-9-]+:v)\d+\.\d+\.\d+/g,
|
|
165
|
+
},
|
|
166
|
+
|
|
123
167
|
// ── engine-private confs (gitignored — bumped only if present). ──
|
|
124
168
|
{
|
|
125
169
|
dir: 'engine-private/conf',
|
|
@@ -448,11 +492,10 @@ class UnderpostRelease {
|
|
|
448
492
|
let commitMsg = message;
|
|
449
493
|
if (!commitMsg) {
|
|
450
494
|
shellCd('/home/dd/engine');
|
|
451
|
-
|
|
495
|
+
commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
|
|
452
496
|
stdout: true,
|
|
453
497
|
silent: true,
|
|
454
498
|
}).trim();
|
|
455
|
-
commitMsg = Underpost.repo.sanitizeChangelogMessage(rawMsg);
|
|
456
499
|
shellCd('/home/dd');
|
|
457
500
|
}
|
|
458
501
|
commitMsg = (commitMsg || '').trim() || `Update ${repoName} repository`;
|
|
@@ -492,11 +535,10 @@ class UnderpostRelease {
|
|
|
492
535
|
let commitMsg = message;
|
|
493
536
|
if (!commitMsg) {
|
|
494
537
|
shellCd('/home/dd/engine');
|
|
495
|
-
|
|
538
|
+
commitMsg = shellExec(`node bin cmt --changelog-msg --changelog-no-hash`, {
|
|
496
539
|
stdout: true,
|
|
497
540
|
silent: true,
|
|
498
541
|
}).trim();
|
|
499
|
-
commitMsg = Underpost.repo.sanitizeChangelogMessage(rawMsg);
|
|
500
542
|
}
|
|
501
543
|
commitMsg = (commitMsg || '').trim() || `Update pwa-microservices-template repository`;
|
|
502
544
|
shellCd('/home/dd');
|
package/src/cli/repository.js
CHANGED
|
@@ -218,7 +218,7 @@ class UnderpostRepository {
|
|
|
218
218
|
return;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
if (options.changelog !== undefined || options.changelogBuild) {
|
|
221
|
+
if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
|
|
222
222
|
const releaseMatch = 'New release v:';
|
|
223
223
|
// Helper: parse [<tag>] commits into grouped sections
|
|
224
224
|
const buildSectionChangelog = (commits) => {
|
|
@@ -255,7 +255,8 @@ class UnderpostRepository {
|
|
|
255
255
|
stdout: true,
|
|
256
256
|
silent: true,
|
|
257
257
|
disableLog: true,
|
|
258
|
-
|
|
258
|
+
silentOnError: true,
|
|
259
|
+
}).toString();
|
|
259
260
|
return rawLog
|
|
260
261
|
.split('\n')
|
|
261
262
|
.map((line) => {
|
|
@@ -346,15 +347,18 @@ class UnderpostRepository {
|
|
|
346
347
|
fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
|
|
347
348
|
logger.info('CHANGELOG.md built at', changelogPath);
|
|
348
349
|
} else {
|
|
349
|
-
// --changelog
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
350
|
+
// --changelog / --changelog-msg: message from the last N commits, where N is --from-n-commit
|
|
351
|
+
// (default 1, last commit only). No auto-detection.
|
|
352
|
+
const n = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
|
|
353
|
+
const sections = buildVersionSections(fetchHistory(n));
|
|
354
|
+
const changelog = renderSections(sections);
|
|
355
|
+
if (options.changelogMsg !== undefined) {
|
|
356
|
+
// Sanitized, commit-ready message; empty string when there are no tagged entries so
|
|
357
|
+
// callers fall back to their own generic default instead of a placeholder.
|
|
358
|
+
console.log(Underpost.repo.sanitizeChangelogMessage(changelog));
|
|
359
|
+
} else {
|
|
360
|
+
console.log(changelog || `No changelog entries found.\n`);
|
|
361
|
+
}
|
|
358
362
|
}
|
|
359
363
|
|
|
360
364
|
return;
|
|
@@ -1257,17 +1261,40 @@ Prevent build private config repo.`,
|
|
|
1257
1261
|
return copiedFiles;
|
|
1258
1262
|
},
|
|
1259
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* Resolves the default branch for a remote GitHub repository by querying
|
|
1266
|
+
* `git ls-remote --symref` and extracting the HEAD ref target.
|
|
1267
|
+
* Falls back to `main` when detection fails.
|
|
1268
|
+
* @param {string} repo - The GitHub repository (e.g., "owner/repo").
|
|
1269
|
+
* @returns {string} The default branch name (e.g. "main" or "master").
|
|
1270
|
+
* @memberof UnderpostRepository
|
|
1271
|
+
*/
|
|
1272
|
+
getDefaultBranch(repo) {
|
|
1273
|
+
if (!repo) return 'main';
|
|
1274
|
+
const authUrl = Underpost.repo.resolveAuthUrl(repo);
|
|
1275
|
+
const raw = shellExec(
|
|
1276
|
+
`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote --symref "${authUrl}" HEAD 2>&1`,
|
|
1277
|
+
{ stdout: true, silent: true, disableLog: true, silentOnError: true },
|
|
1278
|
+
);
|
|
1279
|
+
// --symref emits a line like: ref: refs/heads/main HEAD
|
|
1280
|
+
const match = typeof raw === 'string' ? raw.match(/^ref:\s*refs\/heads\/(\S+)\tHEAD$/m) : null;
|
|
1281
|
+
const branch = match ? match[1] : 'main';
|
|
1282
|
+
logger.info('getDefaultBranch', { repo, branch });
|
|
1283
|
+
return branch;
|
|
1284
|
+
},
|
|
1285
|
+
|
|
1260
1286
|
/**
|
|
1261
1287
|
* Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
|
|
1262
1288
|
* @param {object} options - Dispatch options.
|
|
1263
1289
|
* @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
|
|
1264
1290
|
* @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
|
|
1265
|
-
* @param {string} [options.ref
|
|
1291
|
+
* @param {string} [options.ref] - The git ref to dispatch against. Auto-detects the remote's default branch when omitted.
|
|
1266
1292
|
* @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
|
|
1267
1293
|
* @memberof UnderpostRepository
|
|
1268
1294
|
*/
|
|
1269
|
-
dispatchWorkflow(options = { repo: '', workflowFile: '', ref: '
|
|
1270
|
-
const { repo, workflowFile,
|
|
1295
|
+
dispatchWorkflow(options = { repo: '', workflowFile: '', ref: '', inputs: {} }) {
|
|
1296
|
+
const { repo, workflowFile, inputs } = options;
|
|
1297
|
+
const ref = options.ref || Underpost.repo.getDefaultBranch(repo);
|
|
1271
1298
|
const ghAvailable = shellExec('command -v gh 2>/dev/null', {
|
|
1272
1299
|
stdout: true,
|
|
1273
1300
|
silent: true,
|
|
@@ -1344,14 +1371,18 @@ Prevent build private config repo.`,
|
|
|
1344
1371
|
if (!url) return false;
|
|
1345
1372
|
const authUrl = Underpost.repo.resolveAuthUrl(url);
|
|
1346
1373
|
// GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
|
|
1347
|
-
|
|
1374
|
+
// `-c credential.helper=` disables any host-configured credential helper so its stderr
|
|
1375
|
+
// warnings don't pollute the ls-remote output; the token is already embedded in authUrl.
|
|
1376
|
+
const raw = shellExec(`GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote "${authUrl}" HEAD 2>&1`, {
|
|
1348
1377
|
stdout: true,
|
|
1349
1378
|
silent: true,
|
|
1350
1379
|
disableLog: true,
|
|
1351
1380
|
silentOnError: true,
|
|
1352
1381
|
});
|
|
1353
|
-
|
|
1354
|
-
|
|
1382
|
+
const refLine = typeof raw === 'string' ? (raw.match(/^[0-9a-f]{40}\t.*$/m) || [])[0] : undefined;
|
|
1383
|
+
const accessible = !!refLine;
|
|
1384
|
+
logger.info('isRemoteRepo', { url, accessible, ref: refLine || (raw || '').trim().split('\n').pop() });
|
|
1385
|
+
return accessible;
|
|
1355
1386
|
},
|
|
1356
1387
|
|
|
1357
1388
|
/**
|
|
@@ -1422,17 +1453,31 @@ Prevent build private config repo.`,
|
|
|
1422
1453
|
* @memberof UnderpostRepository
|
|
1423
1454
|
*/
|
|
1424
1455
|
getUnpushedCount(repoPath = '.', fallback = 1) {
|
|
1456
|
+
// Every git call is silentOnError: a detached HEAD (CI checkout) or a missing upstream must
|
|
1457
|
+
// degrade to the fallback, never throw — otherwise the thrown error is logged to stdout and
|
|
1458
|
+
// can be captured as a commit message by callers that read this command's output.
|
|
1425
1459
|
const branch = shellExec(`cd ${repoPath} && git branch --show-current`, {
|
|
1426
1460
|
stdout: true,
|
|
1427
1461
|
silent: true,
|
|
1428
1462
|
disableLog: true,
|
|
1429
|
-
|
|
1430
|
-
|
|
1463
|
+
silentOnError: true,
|
|
1464
|
+
})
|
|
1465
|
+
.toString()
|
|
1466
|
+
.trim();
|
|
1467
|
+
if (!branch) return { count: fallback, branch: '', hasUnpushed: false };
|
|
1468
|
+
shellExec(`cd ${repoPath} && git fetch origin 2>/dev/null`, {
|
|
1469
|
+
silent: true,
|
|
1470
|
+
disableLog: true,
|
|
1471
|
+
silentOnError: true,
|
|
1472
|
+
});
|
|
1431
1473
|
const raw = shellExec(`cd ${repoPath} && git rev-list --count origin/${branch}..HEAD 2>/dev/null`, {
|
|
1432
1474
|
stdout: true,
|
|
1433
1475
|
silent: true,
|
|
1434
1476
|
disableLog: true,
|
|
1435
|
-
|
|
1477
|
+
silentOnError: true,
|
|
1478
|
+
})
|
|
1479
|
+
.toString()
|
|
1480
|
+
.trim();
|
|
1436
1481
|
const count = parseInt(raw);
|
|
1437
1482
|
const hasUnpushed = !isNaN(count) && count > 0;
|
|
1438
1483
|
return { count: hasUnpushed ? count : fallback, branch, hasUnpushed };
|
|
@@ -1447,7 +1492,7 @@ Prevent build private config repo.`,
|
|
|
1447
1492
|
*/
|
|
1448
1493
|
sanitizeChangelogMessage(message) {
|
|
1449
1494
|
if (!message) return '';
|
|
1450
|
-
|
|
1495
|
+
const sanitized = message
|
|
1451
1496
|
.replace(/^##\s+\d{4}-\d{2}-\d{2}\s*/gm, '')
|
|
1452
1497
|
.replace(/^###\s+(\S+)\s*/gm, '[$1] ')
|
|
1453
1498
|
.replace(/^- /gm, '')
|
|
@@ -1459,6 +1504,9 @@ Prevent build private config repo.`,
|
|
|
1459
1504
|
.join('\n')
|
|
1460
1505
|
.trim()
|
|
1461
1506
|
.replaceAll('] - ', '] ');
|
|
1507
|
+
// The empty-changelog placeholder must never become a commit message; return empty so
|
|
1508
|
+
// callers fall back to their own generic default.
|
|
1509
|
+
return sanitized === 'No changelog entries found.' ? '' : sanitized;
|
|
1462
1510
|
},
|
|
1463
1511
|
/**
|
|
1464
1512
|
* Initializes a git repository at the given path and configures user identity
|
|
@@ -1783,12 +1831,17 @@ Prevent build private config repo.`,
|
|
|
1783
1831
|
* 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
|
|
1784
1832
|
*
|
|
1785
1833
|
* @param {string} [runtime=''] - The runtime identifier to look up (e.g. `'cyberia-server'`, `'cyberia-client'`).
|
|
1834
|
+
* @param {boolean} [ownRuntimeRepo=false] - Whether to check for the runtime's own repository.
|
|
1786
1835
|
* @returns {string} The resolved `owner/repo` string.
|
|
1787
1836
|
* @memberof UnderpostRepository
|
|
1788
1837
|
*/
|
|
1789
|
-
resolveInstanceRepo(runtime = '') {
|
|
1838
|
+
resolveInstanceRepo(runtime = '', ownRuntimeRepo = false) {
|
|
1790
1839
|
const fallback = `${process.env.GITHUB_USERNAME}/engine`;
|
|
1791
1840
|
if (!runtime) return fallback;
|
|
1841
|
+
// A `.dev` suffix selects the development image workflow
|
|
1842
|
+
// (docker-image.<runtime>.dev.ci.yml) but resolves to the same instance
|
|
1843
|
+
// repo as its production counterpart, so strip it before matching.
|
|
1844
|
+
runtime = runtime.replace(/\.dev$/, '');
|
|
1792
1845
|
const ddRouter = './engine-private/deploy/dd.router';
|
|
1793
1846
|
const deployIds = fs.existsSync(ddRouter)
|
|
1794
1847
|
? fs
|
|
@@ -1814,6 +1867,16 @@ Prevent build private config repo.`,
|
|
|
1814
1867
|
logger.warn(`[resolveInstanceRepo] failed to parse ${confPath}: ${err.message}`);
|
|
1815
1868
|
}
|
|
1816
1869
|
}
|
|
1870
|
+
if (ownRuntimeRepo) {
|
|
1871
|
+
const runtimeRepo = Underpost.repo.isRemoteRepo(`${process.env.GITHUB_USERNAME}/${runtime}`);
|
|
1872
|
+
if (runtimeRepo) {
|
|
1873
|
+
logger.info(`[resolveInstanceRepo] resolved from ${process.env.GITHUB_USERNAME}/${runtime}`, {
|
|
1874
|
+
runtime,
|
|
1875
|
+
repo: `${process.env.GITHUB_USERNAME}/${runtime}`,
|
|
1876
|
+
});
|
|
1877
|
+
return `${process.env.GITHUB_USERNAME}/${runtime}`;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1817
1880
|
return fallback;
|
|
1818
1881
|
},
|
|
1819
1882
|
|