underpost 3.2.12 → 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/.github/workflows/ghpkg.ci.yml +1 -0
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/CHANGELOG.md +114 -1
- package/CLI-HELP.md +973 -1130
- package/README.md +47 -41
- package/bin/build.js +88 -137
- package/bin/build.template.js +25 -179
- package/bin/deploy.js +4 -1
- package/bin/index.js +2 -2
- package/conf.js +11 -37
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +9 -14
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/test-monitor.sh +86 -0
- package/src/cli/deploy.js +195 -274
- package/src/cli/env.js +1 -4
- package/src/cli/image.js +58 -4
- package/src/cli/index.js +39 -0
- package/src/cli/monitor.js +302 -6
- package/src/cli/release.js +26 -11
- package/src/cli/repository.js +98 -7
- package/src/cli/run.js +137 -69
- package/src/db/mongo/MongooseDB.js +2 -1
- package/src/index.js +1 -1
- package/src/runtime/wp/Dockerfile +3 -3
- package/src/server/catalog-underpost.js +61 -0
- package/src/server/catalog.js +77 -0
- package/src/server/conf.js +365 -56
- package/src/server/runtime-status.js +235 -0
- package/src/server/start.js +17 -8
- package/test/deploy-monitor.test.js +223 -0
- package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
- package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic product-catalog resolver.
|
|
3
|
+
*
|
|
4
|
+
* Product catalogs (`catalog-<suffix>.js`, e.g. `catalog-cyberia`, `catalog-prototype`)
|
|
5
|
+
* are loaded lazily by deploy id via ES dynamic `import()` so the base build
|
|
6
|
+
* (`bin/build`) and template assembly (`bin/build.template`) never statically
|
|
7
|
+
* depend on any product module. Removing a product catalog simply makes its
|
|
8
|
+
* deploy id resolve to the empty catalog — nothing else breaks.
|
|
9
|
+
*
|
|
10
|
+
* Each product catalog default-exports the uniform shape documented in
|
|
11
|
+
* {@link module:src/server/catalog-cyberia}.
|
|
12
|
+
*
|
|
13
|
+
* @module src/server/catalog.js
|
|
14
|
+
* @namespace Catalog
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs-extra';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
|
|
21
|
+
const catalogDir = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
/** Empty product catalog returned for deploy ids without a dedicated module. */
|
|
24
|
+
const EMPTY_CATALOG = {
|
|
25
|
+
sourceMoves: [],
|
|
26
|
+
privateConfPaths: [],
|
|
27
|
+
templatePaths: [],
|
|
28
|
+
stripPaths: [],
|
|
29
|
+
keywords: [],
|
|
30
|
+
description: '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Loads a single deploy id's product catalog. The suffix after `dd-` selects the
|
|
35
|
+
* module (`dd-cyberia` → `catalog-cyberia.js`). Returns {@link EMPTY_CATALOG} when
|
|
36
|
+
* the deploy id has no dedicated catalog or the module cannot be loaded.
|
|
37
|
+
*
|
|
38
|
+
* @method loadDeployCatalog
|
|
39
|
+
* @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`).
|
|
40
|
+
* @returns {Promise<object>} The product catalog (uniform shape).
|
|
41
|
+
* @memberof Catalog
|
|
42
|
+
*/
|
|
43
|
+
const loadDeployCatalog = async (deployId) => {
|
|
44
|
+
const suffix = (deployId ?? '').split('dd-')[1];
|
|
45
|
+
if (!suffix) return EMPTY_CATALOG;
|
|
46
|
+
try {
|
|
47
|
+
const mod = await import(`./catalog-${suffix}.js`);
|
|
48
|
+
return { ...EMPTY_CATALOG, ...(mod.default ?? {}) };
|
|
49
|
+
} catch {
|
|
50
|
+
return EMPTY_CATALOG;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Loads every product catalog present alongside this module (`catalog-*.js`,
|
|
56
|
+
* excluding the base `catalog-underpost` and this resolver). Used to aggregate
|
|
57
|
+
* product `stripPaths` for the base template without naming any product.
|
|
58
|
+
*
|
|
59
|
+
* @method loadProductCatalogs
|
|
60
|
+
* @returns {Promise<object[]>} Loaded product catalogs (uniform shape).
|
|
61
|
+
* @memberof Catalog
|
|
62
|
+
*/
|
|
63
|
+
const loadProductCatalogs = async () => {
|
|
64
|
+
const catalogs = [];
|
|
65
|
+
for (const file of fs.readdirSync(catalogDir)) {
|
|
66
|
+
if (!/^catalog-.+\.js$/.test(file) || file === 'catalog-underpost.js') continue;
|
|
67
|
+
try {
|
|
68
|
+
const mod = await import(`./${file}`);
|
|
69
|
+
if (mod.default) catalogs.push({ ...EMPTY_CATALOG, ...mod.default });
|
|
70
|
+
} catch {
|
|
71
|
+
/* a malformed/removed product catalog must not break the base build */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return catalogs;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { loadDeployCatalog, loadProductCatalogs, EMPTY_CATALOG };
|
package/src/server/conf.js
CHANGED
|
@@ -10,6 +10,7 @@ import dotenv from 'dotenv';
|
|
|
10
10
|
import {
|
|
11
11
|
capFirst,
|
|
12
12
|
getCapVariableName,
|
|
13
|
+
getDirname,
|
|
13
14
|
newInstance,
|
|
14
15
|
orderAbc,
|
|
15
16
|
orderArrayFromAttrInt,
|
|
@@ -1085,13 +1086,9 @@ const buildPortProxyRouter = (
|
|
|
1085
1086
|
|
|
1086
1087
|
if (Object.keys(router).length === 0) return router;
|
|
1087
1088
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
`./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`,
|
|
1092
|
-
'utf8',
|
|
1093
|
-
),
|
|
1094
|
-
);
|
|
1089
|
+
const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
|
|
1090
|
+
if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
|
|
1091
|
+
const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
|
|
1095
1092
|
let devApiHosts = [];
|
|
1096
1093
|
let origins = [];
|
|
1097
1094
|
for (const _host of Object.keys(confDevApiServer))
|
|
@@ -1297,15 +1294,17 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1297
1294
|
* @description Waits for the deploy monitor.
|
|
1298
1295
|
* @param {boolean} [isFinal=false] - If true, logs when the final (non-replica) deployment completes.
|
|
1299
1296
|
* @param {number} [deltaMs=1000] - The delta ms.
|
|
1300
|
-
* @
|
|
1297
|
+
* @param {boolean} [callback=false] - The callback.
|
|
1298
|
+
* @returns {Promise<boolean>} - `false` if `container-status=error` was detected, `true` on clean completion.
|
|
1301
1299
|
* @memberof ServerConfBuilder
|
|
1302
1300
|
*/
|
|
1303
|
-
const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000) => {
|
|
1304
|
-
Underpost.env.set('await-deploy', new Date().toISOString());
|
|
1301
|
+
const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000, callback = false) => {
|
|
1302
|
+
if (!callback) Underpost.env.set('await-deploy', new Date().toISOString());
|
|
1305
1303
|
if (isFinal) logger.info('Final deployment running (no replica)');
|
|
1306
1304
|
await timer(deltaMs);
|
|
1307
|
-
if (Underpost.env.get('container-status') === 'error')
|
|
1308
|
-
if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(
|
|
1305
|
+
if (Underpost.env.get('container-status') === 'error') return false;
|
|
1306
|
+
if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(false, deltaMs, true);
|
|
1307
|
+
return true;
|
|
1309
1308
|
};
|
|
1310
1309
|
|
|
1311
1310
|
/**
|
|
@@ -1421,65 +1420,131 @@ const writeEnv = (envPath, envObj) =>
|
|
|
1421
1420
|
|
|
1422
1421
|
/**
|
|
1423
1422
|
* @method buildCliDoc
|
|
1424
|
-
* @description
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1423
|
+
* @description Scrapes `node bin help` (and `node bin help <command>` for every
|
|
1424
|
+
* registered command) and renders a structured Markdown reference: a command
|
|
1425
|
+
* index with anchor links, plus a per-command section with its description,
|
|
1426
|
+
* usage, and Arguments/Options rendered as tables. Writes
|
|
1427
|
+
* `CLI-HELP.md` + the served reference doc, and refreshes the README CLI index.
|
|
1428
|
+
* @param {object} program - The commander program.
|
|
1429
|
+
* @param {string} oldVersion - The old version string to replace.
|
|
1430
|
+
* @param {string} newVersion - The new version string.
|
|
1428
1431
|
* @memberof ServerConfBuilder
|
|
1429
1432
|
*/
|
|
1430
1433
|
const buildCliDoc = (program, oldVersion, newVersion) => {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
'
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1434
|
+
const help = (args = '') => shellExec(`node bin help${args ? ` ${args}` : ''}`, { silent: true, stdout: true });
|
|
1435
|
+
// Escape table-breaking pipes and collapse wrapped whitespace for a Markdown cell.
|
|
1436
|
+
const cell = (s) => String(s).replace(/\s+/g, ' ').replaceAll('|', '\\|').trim();
|
|
1437
|
+
const anchor = (name) => `underpost-${name}`.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
1438
|
+
|
|
1439
|
+
// Parse a commander help block into { usage, description, sections: { Options, Arguments, Commands } }.
|
|
1440
|
+
const parseHelp = (text) => {
|
|
1441
|
+
const lines = text.split('\n');
|
|
1442
|
+
const usageMatch = lines[0].match(/^Usage:\s*(.*)$/);
|
|
1443
|
+
const usage = usageMatch ? usageMatch[1].trim() : '';
|
|
1444
|
+
const sections = {};
|
|
1445
|
+
const descLines = [];
|
|
1446
|
+
let current = null;
|
|
1447
|
+
let buf = [];
|
|
1448
|
+
const flush = () => {
|
|
1449
|
+
if (current) sections[current] = buf.join('\n');
|
|
1450
|
+
buf = [];
|
|
1451
|
+
};
|
|
1452
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1453
|
+
const line = lines[i];
|
|
1454
|
+
const head = line.match(/^([A-Za-z][\w ]*):\s*$/); // top-level "Options:", "Arguments:", "Commands:"
|
|
1455
|
+
if (head) {
|
|
1456
|
+
flush();
|
|
1457
|
+
current = head[1].trim();
|
|
1458
|
+
} else if (current !== null) {
|
|
1459
|
+
buf.push(line);
|
|
1460
|
+
} else {
|
|
1461
|
+
descLines.push(line);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
flush();
|
|
1465
|
+
return { usage, description: descLines.join('\n').trim(), sections };
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
// Parse a columnar " <term> <description>" section (descriptions may wrap onto
|
|
1469
|
+
// indented continuation lines) into [{ term, desc }].
|
|
1470
|
+
const parseEntries = (text = '') => {
|
|
1471
|
+
const entries = [];
|
|
1472
|
+
for (const line of text.split('\n')) {
|
|
1473
|
+
if (!line.trim()) continue;
|
|
1474
|
+
const leading = line.length - line.trimStart().length;
|
|
1475
|
+
if (leading <= 2) {
|
|
1476
|
+
const rest = line.trim();
|
|
1477
|
+
const gap = rest.search(/\s{2,}/);
|
|
1478
|
+
entries.push(gap === -1 ? { term: rest, desc: '' } : { term: rest.slice(0, gap), desc: rest.slice(gap) });
|
|
1479
|
+
} else if (entries.length) {
|
|
1480
|
+
entries[entries.length - 1].desc += ` ${line.trim()}`;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return entries;
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const table = (head, entries) =>
|
|
1487
|
+
!entries.length
|
|
1488
|
+
? ''
|
|
1489
|
+
: `| ${head[0]} | ${head[1]} |\n| --- | --- |\n` +
|
|
1490
|
+
entries.map(({ term, desc }) => `| \`${cell(term)}\` | ${cell(desc)} |`).join('\n') +
|
|
1491
|
+
'\n';
|
|
1492
|
+
|
|
1493
|
+
const detailSection = (sections, name, head) => {
|
|
1494
|
+
const t = table(head, parseEntries(sections[name]));
|
|
1495
|
+
return t ? `\n#### ${name}\n\n${t}` : '';
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
// ── Top-level index ──
|
|
1499
|
+
const root = parseHelp(help());
|
|
1500
|
+
const commandEntries = parseEntries(root.sections['Commands']).filter((e) => e.term.split(' ')[0] !== 'help');
|
|
1501
|
+
|
|
1502
|
+
const index =
|
|
1503
|
+
`## Underpost CLI\n\n` +
|
|
1504
|
+
(root.description ? `> ${root.description.replace(/\s+/g, ' ')}\n\n` : '') +
|
|
1505
|
+
`**Usage:** \`${root.usage}\`\n\n` +
|
|
1506
|
+
`### Global options\n\n${table(['Option', 'Description'], parseEntries(root.sections['Options']))}\n` +
|
|
1507
|
+
`### Commands\n\n| Command | Description |\n| --- | --- |\n` +
|
|
1508
|
+
commandEntries
|
|
1509
|
+
.map((e) => {
|
|
1510
|
+
const name = e.term.split(' ')[0];
|
|
1511
|
+
return `| [\`${name}\`](#${anchor(name)}) | ${cell(e.desc)} |`;
|
|
1512
|
+
})
|
|
1513
|
+
.join('\n') +
|
|
1514
|
+
'\n';
|
|
1515
|
+
|
|
1516
|
+
// ── Per-command detail ──
|
|
1517
|
+
let details = `\n## Command reference\n`;
|
|
1518
|
+
for (const cmd of program.commands) {
|
|
1519
|
+
const name = cmd._name;
|
|
1520
|
+
if (name === 'help') continue;
|
|
1521
|
+
const cmdHelp = parseHelp(help(name));
|
|
1522
|
+
details +=
|
|
1523
|
+
`\n### underpost ${name}\n\n` +
|
|
1524
|
+
(cmdHelp.description ? `${cmdHelp.description.replace(/\s+/g, ' ')}\n\n` : '') +
|
|
1525
|
+
`**Usage:** \`${cmdHelp.usage}\`\n` +
|
|
1526
|
+
detailSection(cmdHelp.sections, 'Arguments', ['Argument', 'Description']) +
|
|
1527
|
+
detailSection(cmdHelp.sections, 'Options', ['Option', 'Description']) +
|
|
1528
|
+
`\n---\n`;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const md = `${index}${details}`.replaceAll(oldVersion, newVersion);
|
|
1467
1532
|
fs.writeFileSync(`./src/client/public/nexodev/docs/references/Command Line Interface.md`, md, 'utf8');
|
|
1468
1533
|
fs.writeFileSync(`./CLI-HELP.md`, md, 'utf8');
|
|
1469
1534
|
|
|
1470
|
-
// Update README.md:
|
|
1471
|
-
let readme = fs.readFileSync(`./README.md`, 'utf8');
|
|
1472
|
-
readme = readme.replaceAll(oldVersion, newVersion);
|
|
1535
|
+
// Update README.md: bump version and refresh the CLI index between the comment tags.
|
|
1536
|
+
let readme = fs.readFileSync(`./README.md`, 'utf8').replaceAll(oldVersion, newVersion);
|
|
1473
1537
|
const cliStartTag = '<!-- cli-index-start -->';
|
|
1474
1538
|
const cliEndTag = '<!-- cli-index-end -->';
|
|
1475
1539
|
const startIdx = readme.indexOf(cliStartTag);
|
|
1476
1540
|
const endIdx = readme.indexOf(cliEndTag);
|
|
1477
1541
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
1542
|
+
const readmeIndex = index.replace(/\(#(underpost-[a-z0-9-]+)\)/g, '(CLI-HELP.md#$1)');
|
|
1478
1543
|
readme =
|
|
1479
1544
|
readme.substring(0, startIdx) +
|
|
1480
1545
|
cliStartTag +
|
|
1481
1546
|
'\n' +
|
|
1482
|
-
|
|
1547
|
+
readmeIndex.replaceAll(oldVersion, newVersion) +
|
|
1483
1548
|
'\n' +
|
|
1484
1549
|
cliEndTag +
|
|
1485
1550
|
readme.substring(endIdx + cliEndTag.length);
|
|
@@ -1748,6 +1813,245 @@ ${renderHosts}`,
|
|
|
1748
1813
|
return { renderHosts };
|
|
1749
1814
|
};
|
|
1750
1815
|
|
|
1816
|
+
/**
|
|
1817
|
+
* Resolves the concrete deploy ids a build or conf-sync run should iterate over.
|
|
1818
|
+
*
|
|
1819
|
+
* The meta deploy id `dd` fans out to the comma separated ids declared in
|
|
1820
|
+
* `engine-private/deploy/dd.router`; any other value is parsed as a comma separated list.
|
|
1821
|
+
* Entries are trimmed and empties dropped.
|
|
1822
|
+
*
|
|
1823
|
+
* @method resolveDeployList
|
|
1824
|
+
* @param {string} deployId - A deploy id, a comma separated list, or the `dd` meta id.
|
|
1825
|
+
* @returns {string[]} Ordered list of concrete deploy ids.
|
|
1826
|
+
* @memberof ServerConfBuilder
|
|
1827
|
+
*/
|
|
1828
|
+
const resolveDeployList = (deployId) =>
|
|
1829
|
+
(deployId === 'dd' ? fs.readFileSync('./engine-private/deploy/dd.router', 'utf8') : deployId)
|
|
1830
|
+
.split(',')
|
|
1831
|
+
.map((id) => id.trim())
|
|
1832
|
+
.filter(Boolean);
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Syncs a single deploy id's private configuration into its dedicated
|
|
1836
|
+
* `engine-<suffix>-private` repository and pushes the result.
|
|
1837
|
+
*
|
|
1838
|
+
* Idempotent and safe to rerun: the private repo is cloned when missing or reset to a clean
|
|
1839
|
+
* checkout when present, then the deploy id's `conf` folder, matching `replica` and
|
|
1840
|
+
* `itc-scripts` entries, and any caller-supplied `extraPaths` payloads are mirrored. The
|
|
1841
|
+
* commit/push step is a no-op when nothing changed (`silentOnError`).
|
|
1842
|
+
*
|
|
1843
|
+
* @method syncPrivateConf
|
|
1844
|
+
* @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`), not the `dd` meta id.
|
|
1845
|
+
* @param {string[]} [extraPaths=[]] - Extra `./engine-private` payload paths to mirror (from the
|
|
1846
|
+
* deploy's product catalog), kept out of this module so it stays product-agnostic.
|
|
1847
|
+
* @returns {void}
|
|
1848
|
+
* @memberof ServerConfBuilder
|
|
1849
|
+
*/
|
|
1850
|
+
const syncPrivateConf = (deployId, extraPaths = []) => {
|
|
1851
|
+
const suffix = deployId.split('dd-')[1];
|
|
1852
|
+
const privateRepoName = `engine-${suffix}-private`;
|
|
1853
|
+
const privateGitUri = `${process.env.GITHUB_USERNAME}/${privateRepoName}`;
|
|
1854
|
+
const privateRepoPath = `../${privateRepoName}`;
|
|
1855
|
+
|
|
1856
|
+
if (!fs.existsSync(privateRepoPath)) {
|
|
1857
|
+
shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
|
|
1858
|
+
} else {
|
|
1859
|
+
shellExec(`cd ${privateRepoPath} && git checkout . && git clean -f -d && underpost pull . ${privateGitUri}`, {
|
|
1860
|
+
silent: true,
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const confDest = `${privateRepoPath}/conf/${deployId}`;
|
|
1865
|
+
fs.removeSync(confDest);
|
|
1866
|
+
fs.mkdirSync(confDest, { recursive: true });
|
|
1867
|
+
fs.copySync(`./engine-private/conf/${deployId}`, confDest);
|
|
1868
|
+
|
|
1869
|
+
fs.removeSync(`${privateRepoPath}/replica`);
|
|
1870
|
+
for (const payloadDir of ['replica', 'itc-scripts']) {
|
|
1871
|
+
const srcDir = `./engine-private/${payloadDir}`;
|
|
1872
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
1873
|
+
for (const entry of fs.readdirSync(srcDir))
|
|
1874
|
+
if (entry.match(deployId)) fs.copySync(`${srcDir}/${entry}`, `${privateRepoPath}/${payloadDir}/${entry}`);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
|
|
1878
|
+
|
|
1879
|
+
shellExec(
|
|
1880
|
+
`cd ${privateRepoPath}` +
|
|
1881
|
+
` && git add .` +
|
|
1882
|
+
` && underpost cmt . ci engine-core-conf 'Update ${deployId} conf'` +
|
|
1883
|
+
` && underpost push . ${privateGitUri}`,
|
|
1884
|
+
{ silent: true, silentOnError: true },
|
|
1885
|
+
);
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Moves a deploy's public template sources into the engine working tree ahead of
|
|
1890
|
+
* the build copy step. Idempotent and safe to rerun: each move is guarded by
|
|
1891
|
+
* `existsSync`, so already-moved or absent sources are skipped rather than throwing.
|
|
1892
|
+
* The `[src, dest]` pairs come from the deploy's product catalog (passed in), so
|
|
1893
|
+
* this module stays product-agnostic.
|
|
1894
|
+
*
|
|
1895
|
+
* @method syncDeployIdSources
|
|
1896
|
+
* @param {Array<[string, string]>} [sourceMoves=[]] - Public `[src, dest]` move pairs.
|
|
1897
|
+
* @returns {boolean} `true` when any sources were declared, else `false`.
|
|
1898
|
+
* @memberof ServerConfBuilder
|
|
1899
|
+
*/
|
|
1900
|
+
const syncDeployIdSources = (sourceMoves = []) => {
|
|
1901
|
+
if (!sourceMoves.length) return false;
|
|
1902
|
+
for (const dir of ['src/api', 'src/client/components', 'src/client/public', 'src/client/services'])
|
|
1903
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1904
|
+
for (const [src, dest] of sourceMoves) if (fs.existsSync(src)) fs.moveSync(src, dest, { overwrite: true });
|
|
1905
|
+
return true;
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Rebuilds the standalone `pwa-microservices-template` from scratch out of the current
|
|
1910
|
+
* engine source tree.
|
|
1911
|
+
*
|
|
1912
|
+
* Clones the template repo next to the engine when missing, otherwise resets it to a clean
|
|
1913
|
+
* pristine checkout, then syncs every engine-tracked file the template is allowed to carry
|
|
1914
|
+
* ({@link validateTemplatePath}), strips engine-only + product modules, restores the template's
|
|
1915
|
+
* own CI workflows + guest services, and rewrites `package.json` / `package-lock.json` / `README`
|
|
1916
|
+
* so the result is a standalone, installable project. Throws on failure; callers own exit codes.
|
|
1917
|
+
*
|
|
1918
|
+
* Product catalogs are read dynamically ({@link module:src/server/catalog} `loadProductCatalogs`),
|
|
1919
|
+
* so this stays decoupled from — and survives removal of — any product module.
|
|
1920
|
+
*
|
|
1921
|
+
* @method buildTemplate
|
|
1922
|
+
* @param {object} [options]
|
|
1923
|
+
* @param {string} [options.srcPath='./'] - Engine source root to sync from.
|
|
1924
|
+
* @param {string} [options.toPath='../pwa-microservices-template'] - Template output path.
|
|
1925
|
+
* @returns {Promise<void>}
|
|
1926
|
+
* @memberof ServerConfBuilder
|
|
1927
|
+
*/
|
|
1928
|
+
const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
|
|
1929
|
+
const walk = (await import('ignore-walk')).default;
|
|
1930
|
+
const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } = await import('./catalog-underpost.js');
|
|
1931
|
+
const { loadProductCatalogs } = await import('./catalog.js');
|
|
1932
|
+
const githubUsername = process.env.GITHUB_USERNAME;
|
|
1933
|
+
|
|
1934
|
+
logger.info('Build template', { srcPath, toPath });
|
|
1935
|
+
|
|
1936
|
+
const sourceFiles = (
|
|
1937
|
+
await new Promise((resolve) =>
|
|
1938
|
+
walk({ path: srcPath, ignoreFiles: [`.gitignore`], includeEmpty: false, follow: false }, (...args) =>
|
|
1939
|
+
resolve(args[1]),
|
|
1940
|
+
),
|
|
1941
|
+
)
|
|
1942
|
+
).filter((p) => !p.startsWith('.git'));
|
|
1943
|
+
|
|
1944
|
+
// Clone the template from 0 if missing; otherwise reset it to a clean pristine checkout.
|
|
1945
|
+
if (!fs.existsSync(toPath)) {
|
|
1946
|
+
shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
|
|
1947
|
+
} else {
|
|
1948
|
+
shellExec(`cd ${toPath} && git reset && git checkout . && git clean -f -d`);
|
|
1949
|
+
shellExec(`node bin pull ${toPath} ${githubUsername}/pwa-microservices-template`);
|
|
1950
|
+
shellExec(`sudo rm -rf ${toPath}/engine-private`);
|
|
1951
|
+
shellExec(`sudo rm -rf ${toPath}/logs`);
|
|
1952
|
+
}
|
|
1953
|
+
shellExec(`cd ${toPath} && git config core.filemode false`);
|
|
1954
|
+
|
|
1955
|
+
for (const copyPath of sourceFiles) {
|
|
1956
|
+
if (copyPath === 'NaN') continue;
|
|
1957
|
+
const absolutePath = `${srcPath}/${copyPath}`;
|
|
1958
|
+
if (!validateTemplatePath(absolutePath)) continue;
|
|
1959
|
+
|
|
1960
|
+
const folder = getDirname(`${toPath}/${copyPath}`);
|
|
1961
|
+
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
1962
|
+
|
|
1963
|
+
logger.info('build', `${toPath}/${copyPath}`);
|
|
1964
|
+
fs.copyFileSync(absolutePath, `${toPath}/${copyPath}`);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
fs.copySync(`./.vscode`, `${toPath}/.vscode`);
|
|
1968
|
+
fs.copySync(`./src/client/public/default`, `${toPath}/src/client/public/default`);
|
|
1969
|
+
|
|
1970
|
+
// Preserve the template's own README + package.json identity before merging engine metadata.
|
|
1971
|
+
for (const checkoutPath of ['README.md', 'package.json']) shellExec(`cd ${toPath} && git checkout ${checkoutPath}`);
|
|
1972
|
+
|
|
1973
|
+
// Strip each product catalog's `stripPaths` (aggregated dynamically) plus the engine-only
|
|
1974
|
+
// workflows, deploy manifests, and product catalog modules.
|
|
1975
|
+
const productStripPaths = (await loadProductCatalogs()).flatMap((c) => c.stripPaths);
|
|
1976
|
+
for (const deletePath of productStripPaths) {
|
|
1977
|
+
const target = `${toPath}/${deletePath}`;
|
|
1978
|
+
if (fs.existsSync(target)) fs.removeSync(target);
|
|
1979
|
+
}
|
|
1980
|
+
shellExec(`rm -rf ${toPath}/.github`);
|
|
1981
|
+
shellExec(`rm -rf ${toPath}/manifests/deployment/dd-*`);
|
|
1982
|
+
shellExec(`rm -rf ${toPath}/src/server/catalog-*`);
|
|
1983
|
+
|
|
1984
|
+
fs.mkdirSync(`${toPath}/.github/workflows`, { recursive: true });
|
|
1985
|
+
for (const restorePath of TEMPLATE_RESTORE_PATHS) {
|
|
1986
|
+
const dest = `${toPath}/${restorePath}`;
|
|
1987
|
+
if (fs.statSync(restorePath).isDirectory()) fs.copySync(restorePath, dest, { overwrite: true });
|
|
1988
|
+
else fs.copyFileSync(restorePath, dest);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// ── package.json: take engine deps/scripts/version, keep template identity. ──
|
|
1992
|
+
const originPackageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
1993
|
+
const templatePackageJson = JSON.parse(fs.readFileSync(`${toPath}/package.json`, 'utf8'));
|
|
1994
|
+
const templateName = templatePackageJson.name;
|
|
1995
|
+
|
|
1996
|
+
templatePackageJson.dependencies = originPackageJson.dependencies;
|
|
1997
|
+
templatePackageJson.devDependencies = originPackageJson.devDependencies;
|
|
1998
|
+
templatePackageJson.version = originPackageJson.version;
|
|
1999
|
+
templatePackageJson.scripts = originPackageJson.scripts;
|
|
2000
|
+
templatePackageJson.overrides = originPackageJson.overrides;
|
|
2001
|
+
templatePackageJson.name = templateName;
|
|
2002
|
+
templatePackageJson.description = TEMPLATE_DESCRIPTION;
|
|
2003
|
+
templatePackageJson.keywords = TEMPLATE_KEYWORDS;
|
|
2004
|
+
delete templatePackageJson.scripts['build:template'];
|
|
2005
|
+
fs.writeFileSync(`${toPath}/package.json`, JSON.stringify(templatePackageJson, null, 4), 'utf8');
|
|
2006
|
+
|
|
2007
|
+
// ── package-lock.json: mirror engine packages, keep template name/version on the root entry. ──
|
|
2008
|
+
const originPackageLockJson = JSON.parse(fs.readFileSync('./package-lock.json', 'utf8'));
|
|
2009
|
+
const templatePackageLockJson = JSON.parse(fs.readFileSync(`${toPath}/package-lock.json`, 'utf8'));
|
|
2010
|
+
const originBasePackageLock = newInstance(templatePackageLockJson.packages['']);
|
|
2011
|
+
templatePackageLockJson.name = templateName;
|
|
2012
|
+
templatePackageLockJson.version = originPackageLockJson.version;
|
|
2013
|
+
templatePackageLockJson.packages = originPackageLockJson.packages;
|
|
2014
|
+
templatePackageLockJson.packages[''].name = templateName;
|
|
2015
|
+
templatePackageLockJson.packages[''].version = originPackageLockJson.version;
|
|
2016
|
+
templatePackageLockJson.packages[''].hasInstallScript = originBasePackageLock.hasInstallScript;
|
|
2017
|
+
templatePackageLockJson.packages[''].license = originBasePackageLock.license;
|
|
2018
|
+
fs.writeFileSync(`${toPath}/package-lock.json`, JSON.stringify(templatePackageLockJson, null, 4), 'utf8');
|
|
2019
|
+
|
|
2020
|
+
fs.writeFileSync(
|
|
2021
|
+
`${toPath}/README.md`,
|
|
2022
|
+
fs
|
|
2023
|
+
.readFileSync('./README.md', 'utf8')
|
|
2024
|
+
.replace('<!-- template-title -->', '#### Base template for pwa/api-rest projects.'),
|
|
2025
|
+
'utf8',
|
|
2026
|
+
);
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const updatePrivateTemplateRepo = async () => {
|
|
2030
|
+
const templatePath = '/home/dd/pwa-microservices-template';
|
|
2031
|
+
shellExec(`sudo rm -rf ${templatePath}
|
|
2032
|
+
cd /home/dd/engine && npm run build:template
|
|
2033
|
+
cd /home/dd
|
|
2034
|
+
underpost clone --bare underpostnet/pwa-microservices-template-private
|
|
2035
|
+
sudo rm -rf ${templatePath}/.git
|
|
2036
|
+
mv ./pwa-microservices-template-private.git ${templatePath}/.git
|
|
2037
|
+
cd ${templatePath}
|
|
2038
|
+
npm install --omit=dev --ignore-scripts
|
|
2039
|
+
git init
|
|
2040
|
+
git config user.name 'underpostnet'
|
|
2041
|
+
git config user.email 'development@underpost.net'
|
|
2042
|
+
git add .`);
|
|
2043
|
+
const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
|
|
2044
|
+
stdout: true,
|
|
2045
|
+
silent: true,
|
|
2046
|
+
disableLog: true,
|
|
2047
|
+
}).trim();
|
|
2048
|
+
if (hasChanges === '1') {
|
|
2049
|
+
shellExec(
|
|
2050
|
+
`cd ${templatePath} && git commit -m 'Update template' && underpost push . underpostnet/pwa-microservices-template-private`,
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
|
|
1751
2055
|
export {
|
|
1752
2056
|
Config,
|
|
1753
2057
|
loadConf,
|
|
@@ -1795,4 +2099,9 @@ export {
|
|
|
1795
2099
|
loadCronDeployEnv,
|
|
1796
2100
|
cronDeployIdResolve,
|
|
1797
2101
|
etcHostFactory,
|
|
2102
|
+
resolveDeployList,
|
|
2103
|
+
syncPrivateConf,
|
|
2104
|
+
syncDeployIdSources,
|
|
2105
|
+
buildTemplate,
|
|
2106
|
+
updatePrivateTemplateRepo,
|
|
1798
2107
|
};
|