underpost 3.2.28 → 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 +161 -1
- package/CLI-HELP.md +17 -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 +3 -5
- package/src/cli/cluster.js +77 -4
- package/src/cli/deploy.js +121 -9
- package/src/cli/docker-compose.js +58 -1
- package/src/cli/env.js +11 -2
- package/src/cli/fs.js +45 -24
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +35 -6
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +166 -22
- package/src/cli/run.js +273 -118
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +44 -0
- package/src/client.build.js +0 -2
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +0 -3
- package/src/server/runtime-status.js +18 -1
- package/src/server/start.js +12 -2
- package/src/server.js +5 -1
- package/test/deploy-monitor.test.js +26 -10
package/src/cli/repository.js
CHANGED
|
@@ -106,6 +106,9 @@ class UnderpostRepository {
|
|
|
106
106
|
* @param {boolean} [options.changelogBuild=false] - If true, scrapes all git history and builds a full CHANGELOG.md. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
|
|
107
107
|
* @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
|
|
108
108
|
* @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
|
|
109
|
+
* @param {boolean} [options.remoteUrl=false] - If true, prints the current git remote URL (origin) in plain text and returns.
|
|
110
|
+
* @param {string} [options.switchRepo=''] - If set, switches the remote `origin` to this URL and force-pulls the target branch, overwriting the current working tree.
|
|
111
|
+
* @param {string} [options.targetBranch=''] - Target branch for `switchRepo` (defaults to `master`).
|
|
109
112
|
* @memberof UnderpostRepository
|
|
110
113
|
*/
|
|
111
114
|
commit(
|
|
@@ -135,6 +138,9 @@ class UnderpostRepository {
|
|
|
135
138
|
bc: '',
|
|
136
139
|
isRemoteRepo: '',
|
|
137
140
|
hasChanges: false,
|
|
141
|
+
remoteUrl: false,
|
|
142
|
+
switchRepo: '',
|
|
143
|
+
targetBranch: '',
|
|
138
144
|
},
|
|
139
145
|
) {
|
|
140
146
|
if (!repoPath) repoPath = '.';
|
|
@@ -155,6 +161,22 @@ class UnderpostRepository {
|
|
|
155
161
|
return;
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
if (options.remoteUrl) {
|
|
165
|
+
const url = Underpost.repo.getRemoteUrl({ path: repoPath });
|
|
166
|
+
if (options.copy) pbcopy(url);
|
|
167
|
+
else console.log(url);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options.switchRepo) {
|
|
172
|
+
Underpost.repo.switchRemote({
|
|
173
|
+
path: repoPath,
|
|
174
|
+
url: options.switchRepo,
|
|
175
|
+
branch: options.targetBranch || 'master',
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
if (options.bc) {
|
|
159
181
|
console.log(
|
|
160
182
|
shellExec(`cd ${repoPath} && git for-each-ref --contains ${options.bc} --format='%(refname:short)'`, {
|
|
@@ -196,7 +218,7 @@ class UnderpostRepository {
|
|
|
196
218
|
return;
|
|
197
219
|
}
|
|
198
220
|
|
|
199
|
-
if (options.changelog !== undefined || options.changelogBuild) {
|
|
221
|
+
if (options.changelog !== undefined || options.changelogBuild || options.changelogMsg !== undefined) {
|
|
200
222
|
const releaseMatch = 'New release v:';
|
|
201
223
|
// Helper: parse [<tag>] commits into grouped sections
|
|
202
224
|
const buildSectionChangelog = (commits) => {
|
|
@@ -233,7 +255,8 @@ class UnderpostRepository {
|
|
|
233
255
|
stdout: true,
|
|
234
256
|
silent: true,
|
|
235
257
|
disableLog: true,
|
|
236
|
-
|
|
258
|
+
silentOnError: true,
|
|
259
|
+
}).toString();
|
|
237
260
|
return rawLog
|
|
238
261
|
.split('\n')
|
|
239
262
|
.map((line) => {
|
|
@@ -324,15 +347,18 @@ class UnderpostRepository {
|
|
|
324
347
|
fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
|
|
325
348
|
logger.info('CHANGELOG.md built at', changelogPath);
|
|
326
349
|
} else {
|
|
327
|
-
// --changelog
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
}
|
|
336
362
|
}
|
|
337
363
|
|
|
338
364
|
return;
|
|
@@ -1235,17 +1261,40 @@ Prevent build private config repo.`,
|
|
|
1235
1261
|
return copiedFiles;
|
|
1236
1262
|
},
|
|
1237
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
|
+
|
|
1238
1286
|
/**
|
|
1239
1287
|
* Dispatches a GitHub Actions workflow using gh CLI or curl fallback.
|
|
1240
1288
|
* @param {object} options - Dispatch options.
|
|
1241
1289
|
* @param {string} options.repo - The GitHub repository (e.g., "owner/repo").
|
|
1242
1290
|
* @param {string} options.workflowFile - The workflow file name (e.g., "engine-core.cd.yml").
|
|
1243
|
-
* @param {string} [options.ref
|
|
1291
|
+
* @param {string} [options.ref] - The git ref to dispatch against. Auto-detects the remote's default branch when omitted.
|
|
1244
1292
|
* @param {object} [options.inputs={}] - Key-value inputs for the workflow_dispatch event.
|
|
1245
1293
|
* @memberof UnderpostRepository
|
|
1246
1294
|
*/
|
|
1247
|
-
dispatchWorkflow(options = { repo: '', workflowFile: '', ref: '
|
|
1248
|
-
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);
|
|
1249
1298
|
const ghAvailable = shellExec('command -v gh 2>/dev/null', {
|
|
1250
1299
|
stdout: true,
|
|
1251
1300
|
silent: true,
|
|
@@ -1322,14 +1371,77 @@ Prevent build private config repo.`,
|
|
|
1322
1371
|
if (!url) return false;
|
|
1323
1372
|
const authUrl = Underpost.repo.resolveAuthUrl(url);
|
|
1324
1373
|
// GIT_TERMINAL_PROMPT=0 prevents git from hanging on credential prompts inside containers.
|
|
1325
|
-
|
|
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`, {
|
|
1326
1377
|
stdout: true,
|
|
1327
1378
|
silent: true,
|
|
1328
1379
|
disableLog: true,
|
|
1329
1380
|
silentOnError: true,
|
|
1330
1381
|
});
|
|
1331
|
-
|
|
1332
|
-
|
|
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;
|
|
1386
|
+
},
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Returns the current URL of a git remote in plain text.
|
|
1390
|
+
* @param {object} [opts]
|
|
1391
|
+
* @param {string} [opts.path='.'] - Path to the git repository.
|
|
1392
|
+
* @param {string} [opts.remote='origin'] - Remote name to query.
|
|
1393
|
+
* @returns {string} The remote URL, or '' when the remote is not configured.
|
|
1394
|
+
* @memberof UnderpostRepository
|
|
1395
|
+
*/
|
|
1396
|
+
getRemoteUrl({ path: repoPath = '.', remote = 'origin' } = {}) {
|
|
1397
|
+
return shellExec(`cd "${repoPath}" && git remote get-url ${remote}`, {
|
|
1398
|
+
stdout: true,
|
|
1399
|
+
silent: true,
|
|
1400
|
+
disableLog: true,
|
|
1401
|
+
silentOnError: true,
|
|
1402
|
+
}).trim();
|
|
1403
|
+
},
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Switches a local repository onto a different remote and force-syncs its
|
|
1407
|
+
* working tree to a target branch, discarding local commits and tracked
|
|
1408
|
+
* changes — effectively "switch repo to <url>#<branch>".
|
|
1409
|
+
*
|
|
1410
|
+
* Sequence (idempotent, re-runnable):
|
|
1411
|
+
* 1. Normalize the URL (`owner/repo` → full GitHub HTTPS) and set/add the
|
|
1412
|
+
* remote, storing the token-free URL so no secret leaks into `.git/config`.
|
|
1413
|
+
* 2. Force-fetch the target branch (auth injected inline for private repos).
|
|
1414
|
+
* 3. Reset the working tree to the fetched tip and check out the target
|
|
1415
|
+
* branch, overwriting any current tracked content.
|
|
1416
|
+
*
|
|
1417
|
+
* Untracked files are intentionally left in place (no `git clean`).
|
|
1418
|
+
*
|
|
1419
|
+
* @param {object} opts
|
|
1420
|
+
* @param {string} opts.url - New remote URL (full URL or "owner/repo" short form).
|
|
1421
|
+
* @param {string} [opts.path='.'] - Path to the git repository.
|
|
1422
|
+
* @param {string} [opts.branch='master'] - Target branch to overwrite the current tree with.
|
|
1423
|
+
* @param {string} [opts.remote='origin'] - Remote name to set and fetch from.
|
|
1424
|
+
* @returns {void}
|
|
1425
|
+
* @memberof UnderpostRepository
|
|
1426
|
+
*/
|
|
1427
|
+
switchRemote({ url, path: repoPath = '.', branch = 'master', remote = 'origin' }) {
|
|
1428
|
+
if (!url) throw new Error('switchRemote requires a target remote url');
|
|
1429
|
+
if (!fs.existsSync(`${repoPath}/.git`)) throw new Error(`switchRemote: not a git repository: ${repoPath}`);
|
|
1430
|
+
// Token-free URL for the stored remote; auth-injected URL only for the fetch.
|
|
1431
|
+
let normalized = url;
|
|
1432
|
+
if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('git@')) {
|
|
1433
|
+
normalized = `https://github.com/${url}`;
|
|
1434
|
+
}
|
|
1435
|
+
const authUrl = Underpost.repo.resolveAuthUrl(url);
|
|
1436
|
+
const current = Underpost.repo.getRemoteUrl({ path: repoPath, remote });
|
|
1437
|
+
if (!current) shellExec(`cd "${repoPath}" && git remote add ${remote} "${normalized}"`);
|
|
1438
|
+
else shellExec(`cd "${repoPath}" && git remote set-url ${remote} "${normalized}"`);
|
|
1439
|
+
logger.info('switchRemote', { path: repoPath, remote, branch, url: normalized });
|
|
1440
|
+
shellExec(`cd "${repoPath}" && GIT_TERMINAL_PROMPT=0 git fetch --force "${authUrl}" ${branch}`);
|
|
1441
|
+
// reset --hard first clears the worktree so the checkout cannot be blocked
|
|
1442
|
+
// by conflicting local changes; -B points the target branch at the fetched tip.
|
|
1443
|
+
shellExec(`cd "${repoPath}" && git reset --hard FETCH_HEAD`);
|
|
1444
|
+
shellExec(`cd "${repoPath}" && git checkout -B ${branch} FETCH_HEAD`);
|
|
1333
1445
|
},
|
|
1334
1446
|
|
|
1335
1447
|
/**
|
|
@@ -1341,17 +1453,31 @@ Prevent build private config repo.`,
|
|
|
1341
1453
|
* @memberof UnderpostRepository
|
|
1342
1454
|
*/
|
|
1343
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.
|
|
1344
1459
|
const branch = shellExec(`cd ${repoPath} && git branch --show-current`, {
|
|
1345
1460
|
stdout: true,
|
|
1346
1461
|
silent: true,
|
|
1347
1462
|
disableLog: true,
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
+
});
|
|
1350
1473
|
const raw = shellExec(`cd ${repoPath} && git rev-list --count origin/${branch}..HEAD 2>/dev/null`, {
|
|
1351
1474
|
stdout: true,
|
|
1352
1475
|
silent: true,
|
|
1353
1476
|
disableLog: true,
|
|
1354
|
-
|
|
1477
|
+
silentOnError: true,
|
|
1478
|
+
})
|
|
1479
|
+
.toString()
|
|
1480
|
+
.trim();
|
|
1355
1481
|
const count = parseInt(raw);
|
|
1356
1482
|
const hasUnpushed = !isNaN(count) && count > 0;
|
|
1357
1483
|
return { count: hasUnpushed ? count : fallback, branch, hasUnpushed };
|
|
@@ -1366,7 +1492,7 @@ Prevent build private config repo.`,
|
|
|
1366
1492
|
*/
|
|
1367
1493
|
sanitizeChangelogMessage(message) {
|
|
1368
1494
|
if (!message) return '';
|
|
1369
|
-
|
|
1495
|
+
const sanitized = message
|
|
1370
1496
|
.replace(/^##\s+\d{4}-\d{2}-\d{2}\s*/gm, '')
|
|
1371
1497
|
.replace(/^###\s+(\S+)\s*/gm, '[$1] ')
|
|
1372
1498
|
.replace(/^- /gm, '')
|
|
@@ -1378,6 +1504,9 @@ Prevent build private config repo.`,
|
|
|
1378
1504
|
.join('\n')
|
|
1379
1505
|
.trim()
|
|
1380
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;
|
|
1381
1510
|
},
|
|
1382
1511
|
/**
|
|
1383
1512
|
* Initializes a git repository at the given path and configures user identity
|
|
@@ -1702,12 +1831,17 @@ Prevent build private config repo.`,
|
|
|
1702
1831
|
* 4. Falls back to `${GITHUB_USERNAME}/engine` when no match is found.
|
|
1703
1832
|
*
|
|
1704
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.
|
|
1705
1835
|
* @returns {string} The resolved `owner/repo` string.
|
|
1706
1836
|
* @memberof UnderpostRepository
|
|
1707
1837
|
*/
|
|
1708
|
-
resolveInstanceRepo(runtime = '') {
|
|
1838
|
+
resolveInstanceRepo(runtime = '', ownRuntimeRepo = false) {
|
|
1709
1839
|
const fallback = `${process.env.GITHUB_USERNAME}/engine`;
|
|
1710
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$/, '');
|
|
1711
1845
|
const ddRouter = './engine-private/deploy/dd.router';
|
|
1712
1846
|
const deployIds = fs.existsSync(ddRouter)
|
|
1713
1847
|
? fs
|
|
@@ -1733,6 +1867,16 @@ Prevent build private config repo.`,
|
|
|
1733
1867
|
logger.warn(`[resolveInstanceRepo] failed to parse ${confPath}: ${err.message}`);
|
|
1734
1868
|
}
|
|
1735
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
|
+
}
|
|
1736
1880
|
return fallback;
|
|
1737
1881
|
},
|
|
1738
1882
|
|