@bongos/core 1.19.748 → 1.19.749
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/.bongos-core.json +13 -13
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/upgrade.js +36 -12
- package/src/module-api.js +1 -1
- package/tests/upgrade.mjs +39 -1
- package/tests/upgrade_persist_pin.mjs +102 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.749",
|
|
6
|
+
"core_contract": "1.19.749",
|
|
7
|
+
"source_commit": "238806532985c069a1b8a778e5a3d223cd6b01c4",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-15T22:00:53.285Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 482,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2616,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "ddc2e6cffdc346ee09a971dd7f237459cf8c142ad8cf058845f48638fa01a3e7",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2507,7 +2507,7 @@
|
|
|
2507
2507
|
{
|
|
2508
2508
|
"path": "docs/module-api-changelog.md",
|
|
2509
2509
|
"mode": "0000644",
|
|
2510
|
-
"sha256": "
|
|
2510
|
+
"sha256": "207ce89a17ab5bd75cb956f9e41497ef39eb10d4f59eb634f0d1c1933304ced0"
|
|
2511
2511
|
},
|
|
2512
2512
|
{
|
|
2513
2513
|
"path": "docs/modules-contract.md",
|
|
@@ -2767,7 +2767,7 @@
|
|
|
2767
2767
|
{
|
|
2768
2768
|
"path": "docs/recipes/upgrading-the-core.md",
|
|
2769
2769
|
"mode": "0000644",
|
|
2770
|
-
"sha256": "
|
|
2770
|
+
"sha256": "408b4792ffb869f2fa13201485c1ab5b8306b93996bb75f01b4117de3fd39303"
|
|
2771
2771
|
},
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/recipes/windows-builders.md",
|
|
@@ -7562,12 +7562,12 @@
|
|
|
7562
7562
|
{
|
|
7563
7563
|
"path": "package-lock.json",
|
|
7564
7564
|
"mode": "0000644",
|
|
7565
|
-
"sha256": "
|
|
7565
|
+
"sha256": "1cf816c4ca4b4b56d3fefe33e86e8ce4caa511ee69fd8dd6c2ecc2940dc07f7f"
|
|
7566
7566
|
},
|
|
7567
7567
|
{
|
|
7568
7568
|
"path": "package.json",
|
|
7569
7569
|
"mode": "0000644",
|
|
7570
|
-
"sha256": "
|
|
7570
|
+
"sha256": "d890fe5b32e9b8158a19a40b765570972673f8558cb5d2e8e13ba396eee4476f"
|
|
7571
7571
|
},
|
|
7572
7572
|
{
|
|
7573
7573
|
"path": "public-docs/index.html",
|
|
@@ -8927,7 +8927,7 @@
|
|
|
8927
8927
|
{
|
|
8928
8928
|
"path": "scripts/gds/upgrade.js",
|
|
8929
8929
|
"mode": "0000644",
|
|
8930
|
-
"sha256": "
|
|
8930
|
+
"sha256": "fe3abe00b43163eb1b24dd2da57d4c8fd51194e79bab8a6683355980787a9e8c"
|
|
8931
8931
|
},
|
|
8932
8932
|
{
|
|
8933
8933
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -9367,7 +9367,7 @@
|
|
|
9367
9367
|
{
|
|
9368
9368
|
"path": "src/module-api.js",
|
|
9369
9369
|
"mode": "0000644",
|
|
9370
|
-
"sha256": "
|
|
9370
|
+
"sha256": "6c9cce81108782c099d58259ce776bed22f815f7276fa9fb9a50264aeedd3835"
|
|
9371
9371
|
},
|
|
9372
9372
|
{
|
|
9373
9373
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12942,12 +12942,12 @@
|
|
|
12942
12942
|
{
|
|
12943
12943
|
"path": "tests/upgrade.mjs",
|
|
12944
12944
|
"mode": "0000644",
|
|
12945
|
-
"sha256": "
|
|
12945
|
+
"sha256": "bcbba7b1c952088f43300405184849bbcbb2da789097fcc4cf38d95441e0c158"
|
|
12946
12946
|
},
|
|
12947
12947
|
{
|
|
12948
12948
|
"path": "tests/upgrade_persist_pin.mjs",
|
|
12949
12949
|
"mode": "0000644",
|
|
12950
|
-
"sha256": "
|
|
12950
|
+
"sha256": "864b9471fcd6bfc41e99fd5356a3124c25d288946326c2797cc2bacb86120d05"
|
|
12951
12951
|
},
|
|
12952
12952
|
{
|
|
12953
12953
|
"path": "tests/verify_task_guard.mjs",
|
|
@@ -1970,5 +1970,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1970
1970
|
landed since 1.19.746 with no explicit bump. run 35025151628. (task 1002620)
|
|
1971
1971
|
1.19.748 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1972
1972
|
landed since 1.19.747 with no explicit bump. run 35026862340. (task 1002620)
|
|
1973
|
+
1.19.749 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1974
|
+
landed since 1.19.748 with no explicit bump. run 35028684745. (task 1002620)
|
|
1973
1975
|
---------------------------------------------------------------------------
|
|
1974
1976
|
```
|
|
@@ -55,7 +55,7 @@ The instance runs on the droplet (co-tenant): systemd `emersonian-circles.servic
|
|
|
55
55
|
|
|
56
56
|
> **The pin MUST reach the repo of record, or the bump silently un-does itself** (task 1002712). A pull-deploy instance resets to `origin/main`; a pin that lives only in the working tree is reverted the next time anything lands, and the lockfile change then trips `npm ci`, which reinstalls the OLD core. Nothing errors. cloudbongos.com lost thirteen releases exactly this way — the `core_upgrades` ledger held two successful `auto-subscription (patch)` rows whose result no longer existed on disk, because deploy.sh short-circuits while `HEAD == origin/main` so the upgrade *looks* fine right up until someone else pushes.
|
|
57
57
|
>
|
|
58
|
-
> `bongos upgrade` now **warns loudly** whenever it leaves the pin uncommitted in a git repo, and `--commit-pin` (optionally `--git-remote <name>`) commits + pushes `package.json` + `package-lock.json` for you. A commit that fails to push is reported as a failure too — deploy.sh resets to the *remote*, so committing locally saves nothing. Verify durability by forcing a deploy tick (`sudo systemctl start <instance>-deploy.service`) and re-reading the pin.
|
|
58
|
+
> `bongos upgrade` now **warns loudly** whenever it leaves the pin uncommitted in a git repo, and `--commit-pin` (optionally `--git-remote <name>`) commits + pushes `package.json` + `package-lock.json` for you, together with any regenerated API spec, reference and typed client the bump rewrote (task 1003926 — left uncommitted, they made the next upgrade fail its clean-tree pre-flight). A commit that fails to push is reported as a failure too — deploy.sh resets to the *remote*, so committing locally saves nothing. Verify durability by forcing a deploy tick (`sudo systemctl start <instance>-deploy.service`) and re-reading the pin.
|
|
59
59
|
7. **Align the droplet checkout** — the droplet has no deploy key (`git pull` fails; idea 509), so `git bundle` the new commit on the laptop, `scp`, `git fetch <bundle> && git reset --hard FETCH_HEAD`. `node_modules` is already the new version, so no re-`npm ci`; a final restart makes `/version` report the new commit.
|
|
60
60
|
|
|
61
61
|
## Auto-rollback on failure (task 2149)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.749",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.749",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.749",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -157,6 +157,17 @@ function gitTreeClean(instanceDir, run = spawnSync) {
|
|
|
157
157
|
// Hence: --commit-pin lands it, and its absence WARNS rather than staying quiet.
|
|
158
158
|
const PIN_FILES = ['package.json', 'package-lock.json'];
|
|
159
159
|
|
|
160
|
+
// The tracked files regenerateApiArtifacts' generators rewrite WHOLE, by generator (task
|
|
161
|
+
// 1003926). Committing only the pin left these dirty, so the NEXT `bongos upgrade` refused
|
|
162
|
+
// its clean-tree pre-flight. They are generated end to end, so once a generator has run
|
|
163
|
+
// their content is its output whatever they held before: committing them can never carry
|
|
164
|
+
// an owner's edit. tests/upgrade_persist_pin.mjs pins this list against both generators.
|
|
165
|
+
const API_ARTIFACT_FILES = {
|
|
166
|
+
'gen-api-docs.js': ['docs/api/openapi.json', 'docs/api-reference.md', 'docs/routes-permissions.md'],
|
|
167
|
+
'gen-api-client.js': ['index.mjs', 'index.cjs', 'bongos-client.global.js', 'index.d.ts', 'package.json', 'README.md']
|
|
168
|
+
.map((f) => `clients/bongos-client/${f}`),
|
|
169
|
+
};
|
|
170
|
+
|
|
160
171
|
function gitCurrentBranch(instanceDir, run = spawnSync) {
|
|
161
172
|
const r = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: instanceDir, encoding: 'utf8' });
|
|
162
173
|
if (!r || r.error || r.status !== 0) return null;
|
|
@@ -164,13 +175,17 @@ function gitCurrentBranch(instanceDir, run = spawnSync) {
|
|
|
164
175
|
return b && b !== 'HEAD' ? b : null;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
// Which of
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// Which of `paths` git currently reports as changed — never a path it was not asked about,
|
|
179
|
+
// and no git call for an empty list (a pathspec of nothing is the whole tree).
|
|
180
|
+
function dirtyPaths(instanceDir, paths, run = spawnSync) {
|
|
181
|
+
if (!paths.length) return [];
|
|
182
|
+
const r = run('git', ['status', '--porcelain', '--', ...paths], { cwd: instanceDir, encoding: 'utf8' });
|
|
170
183
|
if (!r || r.error || r.status !== 0) return null; // not a git repo — nothing to persist
|
|
171
|
-
return (r.stdout || '').split('\n').map((l) => l.slice(3).trim()).filter(
|
|
184
|
+
return (r.stdout || '').split('\n').map((l) => l.slice(3).trim()).filter((p) => paths.includes(p));
|
|
172
185
|
}
|
|
173
186
|
|
|
187
|
+
function dirtyPinFiles(instanceDir, run = spawnSync) { return dirtyPaths(instanceDir, PIN_FILES, run); }
|
|
188
|
+
|
|
174
189
|
// ---- which credential the pin push rides (task 1003081, ADR 0176 follow-up) ----
|
|
175
190
|
//
|
|
176
191
|
// THE BUG THIS EXISTS FOR. A PRIVATE standalone instance's `origin` authenticates with
|
|
@@ -285,11 +300,16 @@ function gitSaid(r) {
|
|
|
285
300
|
// GitHub token over HTTPS (task 1003081); null pushes via `remote`. The result carries
|
|
286
301
|
// `via: 'owner-token'` on the token path and `refused` ('read-only-deploy-key' |
|
|
287
302
|
// 'owner-token') with a plain-sentence `error` when GitHub turned the push away.
|
|
288
|
-
|
|
303
|
+
// `artifactPaths` (task 1003926) are the generated files this upgrade rewrote; the ones
|
|
304
|
+
// that changed are staged and committed with the pin.
|
|
305
|
+
function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = true, remote = 'origin', dryRun = false, pushAuth = null, artifactPaths = [] },
|
|
289
306
|
run = spawnSync, log = console.log, err = console.error) {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
307
|
+
const dirtyPins = dirtyPinFiles(instanceDir, run);
|
|
308
|
+
if (dirtyPins === null) return { applicable: false, reason: 'not a git repo' };
|
|
309
|
+
const dirtyArtifacts = dirtyPaths(instanceDir, artifactPaths, run) || [];
|
|
310
|
+
const dirty = [...dirtyPins, ...dirtyArtifacts];
|
|
292
311
|
if (!dirty.length) return { applicable: false, reason: 'pin files already match the repo' };
|
|
312
|
+
const toStage = [...PIN_FILES, ...dirtyArtifacts];
|
|
293
313
|
|
|
294
314
|
const branch = gitCurrentBranch(instanceDir, run);
|
|
295
315
|
const target = `HEAD:${branch || 'main'}`;
|
|
@@ -314,7 +334,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
314
334
|
err(' On a pull-deploy instance the next push to the repo of record makes deploy.sh');
|
|
315
335
|
err(` 'git reset --hard ${remote}/${branch || 'main'}', which REVERTS this pin and lets npm ci`);
|
|
316
336
|
err(` reinstall ${fromVersion || 'the previous core'}. Re-run with --commit-pin, or commit + push:`);
|
|
317
|
-
err(` ${at} add ${
|
|
337
|
+
err(` ${at} add ${toStage.map(q).join(' ')} &&${at} commit -m ${q(`pin @bongos/core ${toVersion}`)} && ${at} push ${q(remote)} ${q(target)}`);
|
|
318
338
|
return { applicable: true, committed: false, pushed: false, dirty, warned: true };
|
|
319
339
|
}
|
|
320
340
|
|
|
@@ -326,7 +346,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
326
346
|
}
|
|
327
347
|
|
|
328
348
|
const message = `pin @bongos/core ${toVersion}${fromVersion ? ` (was ${fromVersion})` : ''}`;
|
|
329
|
-
const add = run('git', ['add', ...
|
|
349
|
+
const add = run('git', ['add', ...toStage], { cwd: instanceDir, encoding: 'utf8' });
|
|
330
350
|
if (!add || add.error || add.status !== 0) {
|
|
331
351
|
err(' ! could not stage the pin files — commit + push them by hand or the next deploy reverts this bump.');
|
|
332
352
|
return { applicable: true, committed: false, pushed: false, dirty, error: 'git add failed' };
|
|
@@ -572,7 +592,7 @@ function resolveInstanceDb({ instanceDir }, run = spawnSync) {
|
|
|
572
592
|
function regenerateApiArtifacts({ instanceDir, coreRoot }, run = spawnSync, fsImpl = fs) {
|
|
573
593
|
const env = { ...process.env, BONGOS_INSTANCE_ROOT: instanceDir };
|
|
574
594
|
const ran = [];
|
|
575
|
-
for (const gen of
|
|
595
|
+
for (const gen of Object.keys(API_ARTIFACT_FILES)) {
|
|
576
596
|
const script = path.join(coreRoot, 'scripts', 'gds', gen);
|
|
577
597
|
if (!fsImpl.existsSync(script)) return { ok: false, ran, reason: `${gen} not found in core (${coreRoot})` };
|
|
578
598
|
const r = run(process.execPath, [script], { cwd: instanceDir, env, stdio: 'inherit' });
|
|
@@ -1185,9 +1205,11 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1185
1205
|
// 5b. regenerate the per-instance API artifacts (OpenAPI spec + typed client) from the
|
|
1186
1206
|
// new core's routes — the claude-materialize sibling (task 2051). Best-effort: a regen
|
|
1187
1207
|
// hiccup is a warning, not a failed bump (the next ship's fitness gate refreshes them).
|
|
1208
|
+
let regenArtifacts = [];
|
|
1188
1209
|
if (!opts.skipRegenDocs) {
|
|
1189
1210
|
const coreRoot = path.join(instanceDir, 'node_modules', CORE_PKG);
|
|
1190
1211
|
const regen = (deps.regenerateApiArtifacts || regenerateApiArtifacts)({ instanceDir, coreRoot }, run);
|
|
1212
|
+
regenArtifacts = (regen.ran || []).flatMap((gen) => API_ARTIFACT_FILES[gen] || []);
|
|
1191
1213
|
if (regen.ok) log(` ✓ regenerated API docs + typed client from core ${targetVersion}`);
|
|
1192
1214
|
else err(` ! API-artifact regen incomplete (${regen.reason}) — ran ${regen.ran.join(', ') || 'nothing'}; the next ship's fitness gate refreshes them.`);
|
|
1193
1215
|
// 5c. regenerate the whole-file navigation docs (repo-map + session-index) from the new
|
|
@@ -1301,7 +1323,8 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1301
1323
|
// actually coming: --commit-pin without --pin-no-push, not a dry run, and the pin
|
|
1302
1324
|
// files really are dirty. Nothing reads a credential it will not spend (task 1003081).
|
|
1303
1325
|
const remote = opts.gitRemote || 'origin';
|
|
1304
|
-
const pushComing = !!opts.commitPin && !opts.pinNoPush && !dryRun
|
|
1326
|
+
const pushComing = !!opts.commitPin && !opts.pinNoPush && !dryRun
|
|
1327
|
+
&& ((dirtyPinFiles(instanceDir, run) || []).length > 0 || (dirtyPaths(instanceDir, regenArtifacts, run) || []).length > 0);
|
|
1305
1328
|
const pushAuth = pushComing ? await (deps.resolvePinPushAuth || resolvePinPushAuth)({ instanceDir, remote }, deps, run) : null;
|
|
1306
1329
|
const pin = persistPin({
|
|
1307
1330
|
instanceDir,
|
|
@@ -1312,6 +1335,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1312
1335
|
remote,
|
|
1313
1336
|
dryRun: opts.dryRun,
|
|
1314
1337
|
pushAuth,
|
|
1338
|
+
artifactPaths: regenArtifacts,
|
|
1315
1339
|
}, run, log, err);
|
|
1316
1340
|
|
|
1317
1341
|
const ok = versionOk && healthOk !== false && servedOk !== false;
|
|
@@ -1394,7 +1418,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1394
1418
|
module.exports = {
|
|
1395
1419
|
runUpgrade, parseArgs, main, rollback,
|
|
1396
1420
|
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
1397
|
-
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity,
|
|
1421
|
+
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, API_ARTIFACT_FILES, preflightDbIdentity,
|
|
1398
1422
|
resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth, deriveVersionUrl, pollServedVersion,
|
|
1399
1423
|
resolvePinPushAuth, remoteGithubRepo, pinnedDeployKey, // task 1003081 — the pin push prefers the owner's GitHub token (ADR 0176 follow-up)
|
|
1400
1424
|
readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.749'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/upgrade.mjs
CHANGED
|
@@ -228,7 +228,9 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
|
|
|
228
228
|
// pin is committed (task 1002712). It runs LAST — after verify — and without
|
|
229
229
|
// --commit-pin it only warns, so no git add/commit/push appears here.
|
|
230
230
|
['git status --porcelain', 'npm install --no-audit --no-fund', 'npm run migrate', 'systemctl restart emersonian.service',
|
|
231
|
-
'git status --porcelain -- package.json package-lock.json'
|
|
231
|
+
'git status --porcelain -- package.json package-lock.json',
|
|
232
|
+
// task 1003926: both generators ran, so the files they rewrote are the pin commit's too
|
|
233
|
+
`git status --porcelain -- ${Object.values(u.API_ARTIFACT_FILES).flat().join(' ')}`]
|
|
232
234
|
);
|
|
233
235
|
// materialize pointed at the freshly-installed core
|
|
234
236
|
assert.ok(materialized && materialized.coreRoot.endsWith(join('node_modules', '@bongos', 'core')));
|
|
@@ -1676,6 +1678,42 @@ t('runUpgrade: the token store is consulted only when a push is actually coming'
|
|
|
1676
1678
|
}
|
|
1677
1679
|
});
|
|
1678
1680
|
|
|
1681
|
+
// task 1003926 — the pin commit carries the files the generators that RAN rewrote, and no others.
|
|
1682
|
+
t('runUpgrade --commit-pin --pin-no-push: a generator that did not run contributes nothing to the pin commit', async () => {
|
|
1683
|
+
const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
|
|
1684
|
+
const src = mkdtempSync(join(tmpdir(), 'otb-upgrade-src-'));
|
|
1685
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
|
|
1686
|
+
const calls = [];
|
|
1687
|
+
const changed = ['package.json', 'package-lock.json', 'docs/api/openapi.json', 'clients/bongos-client/package.json'];
|
|
1688
|
+
const run = (cmd, args) => {
|
|
1689
|
+
calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
|
|
1690
|
+
if (cmd === 'git' && args[0] === 'status') {
|
|
1691
|
+
if (!args.includes('--')) return { status: 0, stdout: '' }; // pre-flight: clean
|
|
1692
|
+
const asked = args.slice(args.indexOf('--') + 1);
|
|
1693
|
+
return { status: 0, stdout: changed.filter((p) => asked.includes(p)).map((p) => ` M ${p}\n`).join('') };
|
|
1694
|
+
}
|
|
1695
|
+
if (cmd === 'git' && args[0] === 'rev-parse') return { status: 0, stdout: 'main\n' };
|
|
1696
|
+
if (cmd === 'npm' && args[0] === 'install') {
|
|
1697
|
+
writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version: '1.15.0' }) + '\n');
|
|
1698
|
+
}
|
|
1699
|
+
return { status: 0 };
|
|
1700
|
+
};
|
|
1701
|
+
const res = await u.runUpgrade(
|
|
1702
|
+
{ to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'acme.service', healthCheck: false, commitPin: true, pinNoPush: true },
|
|
1703
|
+
{
|
|
1704
|
+
log: hush, err: hush, run, materialize: () => ({}),
|
|
1705
|
+
// the client generator failed, so its files are not the upgrade's output
|
|
1706
|
+
regenerateApiArtifacts: () => ({ ok: false, ran: ['gen-api-docs.js'], reason: 'gen-api-client.js exited 1' }),
|
|
1707
|
+
databaseUrl: '', pgDatabase: '',
|
|
1708
|
+
},
|
|
1709
|
+
);
|
|
1710
|
+
assert.equal(res.ok, true, `upgrade should succeed: ${res.error}`);
|
|
1711
|
+
assert.equal(res.pin && res.pin.committed, true);
|
|
1712
|
+
assert.ok(calls.includes('git add package.json package-lock.json docs/api/openapi.json'),
|
|
1713
|
+
`got ${JSON.stringify(calls.filter((c) => c.startsWith('git add')))}`);
|
|
1714
|
+
assert.ok(!calls.some((c) => c.startsWith('git push')), '--pin-no-push');
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1679
1717
|
await Promise.all(pending);
|
|
1680
1718
|
console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
|
|
1681
1719
|
process.exit(failed ? 1 : 0);
|
|
@@ -21,9 +21,14 @@
|
|
|
21
21
|
import { test } from 'node:test';
|
|
22
22
|
import assert from 'node:assert/strict';
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { API_ARTIFACT_FILES } from '../scripts/gds/upgrade.js';
|
|
24
29
|
|
|
25
30
|
const require = createRequire(import.meta.url);
|
|
26
|
-
const { persistPin, dirtyPinFiles, preflightDbIdentity, PIN_FILES } = require('../scripts/gds/upgrade.js');
|
|
31
|
+
const { persistPin, dirtyPinFiles, preflightDbIdentity, gitTreeClean, PIN_FILES } = require('../scripts/gds/upgrade.js');
|
|
27
32
|
|
|
28
33
|
// A fake `run` driven by a table of [matcher, result]. Records every invocation.
|
|
29
34
|
function fakeRun(handlers = {}) {
|
|
@@ -250,3 +255,99 @@ test('dirtyPinFiles only ever reports the pin files', () => {
|
|
|
250
255
|
const statusCall = run.calls.find((c) => c.startsWith('git status'));
|
|
251
256
|
assert.match(statusCall, /--\s+package\.json package-lock\.json$/, 'pathspec-limited');
|
|
252
257
|
});
|
|
258
|
+
|
|
259
|
+
// ── the upgrade's own regenerated files ride the pin commit (task 1003926) ───────
|
|
260
|
+
//
|
|
261
|
+
// regenerateApiArtifacts rewrites the instance's TRACKED spec, reference and typed client
|
|
262
|
+
// WHOLE. With only the pin committed they stayed dirty, and the next `bongos upgrade`
|
|
263
|
+
// refused its clean-tree pre-flight: the stranding the pin commit exists to prevent.
|
|
264
|
+
|
|
265
|
+
const ARTIFACTS = Object.values(API_ARTIFACT_FILES).flat();
|
|
266
|
+
// A `git status --porcelain -- <paths>` fake that reports only what was asked AND changed.
|
|
267
|
+
const statusOf = (changed) => (args) => {
|
|
268
|
+
const asked = args.slice(args.indexOf('--') + 1);
|
|
269
|
+
return { status: 0, stdout: changed.filter((p) => asked.includes(p)).map((p) => ` M ${p}\n`).join('') };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
test('API_ARTIFACT_FILES names exactly the files the two generators write', () => {
|
|
273
|
+
const client = require('../scripts/gds/gen-api-client.js').build();
|
|
274
|
+
assert.deepEqual(API_ARTIFACT_FILES['gen-api-client.js'], Object.keys(client.files).map((f) => `clients/bongos-client/${f}`));
|
|
275
|
+
const docsSrc = readFileSync(new URL('../scripts/gds/gen-api-docs.js', import.meta.url), 'utf8');
|
|
276
|
+
assert.deepEqual(API_ARTIFACT_FILES['gen-api-docs.js'], [...docsSrc.matchAll(/\brel: '([^']+)'/g)].map((m) => m[1]));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('the generated files that changed are staged and committed WITH the pin', () => {
|
|
280
|
+
const run = fakeRun({ status: statusOf([...PIN_FILES, 'docs/api/openapi.json', 'clients/bongos-client/package.json']), 'rev-parse': ON_MAIN });
|
|
281
|
+
const r = persistPin({ ...base, commitPin: true, push: false, artifactPaths: ARTIFACTS }, run, quiet, quiet);
|
|
282
|
+
assert.equal(r.committed, true);
|
|
283
|
+
assert.deepEqual(r.dirty, [...PIN_FILES, 'docs/api/openapi.json', 'clients/bongos-client/package.json']);
|
|
284
|
+
assert.ok(run.calls.includes(`git add ${PIN_FILES.join(' ')} docs/api/openapi.json clients/bongos-client/package.json`),
|
|
285
|
+
`stages the pin + only the generated files that changed; got ${JSON.stringify(run.calls)}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('generated files that did not change add nothing to the staging argv', () => {
|
|
289
|
+
const run = fakeRun({ status: statusOf([...PIN_FILES]), 'rev-parse': ON_MAIN });
|
|
290
|
+
persistPin({ ...base, commitPin: true, push: false, artifactPaths: ARTIFACTS }, run, quiet, quiet);
|
|
291
|
+
assert.ok(run.calls.includes(`git add ${PIN_FILES.join(' ')}`));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('an already-committed pin still commits the generated files the upgrade rewrote', () => {
|
|
295
|
+
const run = fakeRun({ status: statusOf(['docs/api-reference.md']), 'rev-parse': ON_MAIN });
|
|
296
|
+
const r = persistPin({ ...base, commitPin: true, push: false, artifactPaths: ARTIFACTS }, run, quiet, quiet);
|
|
297
|
+
assert.equal(r.committed, true, 'a dirty generated file alone is still the upgrade\'s to persist');
|
|
298
|
+
assert.ok(run.calls.includes(`git add ${PIN_FILES.join(' ')} docs/api-reference.md`));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('a path nobody asked about is never reported, and no artifact paths means no extra git call', () => {
|
|
302
|
+
const run = fakeRun({ status: { status: 0, stdout: ' M config/branding.json\n M docs/api/openapi.json\n' }, 'rev-parse': ON_MAIN });
|
|
303
|
+
const r = persistPin({ ...base, commitPin: true, push: false, artifactPaths: ['docs/api/openapi.json'] }, run, quiet, quiet);
|
|
304
|
+
assert.deepEqual(r.dirty, ['docs/api/openapi.json'], 'config/branding.json is not the upgrade\'s to commit');
|
|
305
|
+
const bare = fakeRun({ status: DIRTY, 'rev-parse': ON_MAIN });
|
|
306
|
+
persistPin({ ...base, commitPin: true, push: false }, bare, quiet, quiet);
|
|
307
|
+
assert.equal(bare.calls.filter((c) => c.startsWith('git status')).length, 1, 'an empty pathspec would mean the WHOLE tree');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('without --commit-pin, the paste-ready fix stages the changed generated files too', () => {
|
|
311
|
+
const warnings = [];
|
|
312
|
+
const run = fakeRun({ status: statusOf([...PIN_FILES, 'docs/api/openapi.json']), 'rev-parse': ON_MAIN });
|
|
313
|
+
persistPin({ ...base, commitPin: false, artifactPaths: ARTIFACTS }, run, quiet, (m) => warnings.push(m));
|
|
314
|
+
const cmd = warnings.find((w) => w.includes('git -C'));
|
|
315
|
+
assert.match(cmd, /add 'package\.json' 'package-lock\.json' 'docs\/api\/openapi\.json' &&/);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Real git, no fakes: the task's done-when, reproduced on a scratch repo.
|
|
319
|
+
function git(dir, ...args) { return spawnSync('git', args, { cwd: dir, encoding: 'utf8' }); }
|
|
320
|
+
function put(dir, rel, content) { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), content); }
|
|
321
|
+
function scratchInstance() {
|
|
322
|
+
const dir = mkdtempSync(join(tmpdir(), 'upgrade-regen-'));
|
|
323
|
+
git(dir, 'init', '-q');
|
|
324
|
+
git(dir, 'config', 'user.email', 'upgrade-test@example.invalid');
|
|
325
|
+
git(dir, 'config', 'user.name', 'upgrade test');
|
|
326
|
+
git(dir, 'config', 'core.autocrlf', 'false');
|
|
327
|
+
for (const f of [...PIN_FILES, ...ARTIFACTS, 'config/branding.json']) put(dir, f, 'scaffold\n');
|
|
328
|
+
git(dir, 'add', '-A');
|
|
329
|
+
git(dir, 'commit', '-q', '-m', 'scaffold');
|
|
330
|
+
return dir;
|
|
331
|
+
}
|
|
332
|
+
// What one upgrade writes: the moved pin, and the generators' whole files.
|
|
333
|
+
const upgradeWrites = (dir, version) => { for (const f of [...PIN_FILES, ...ARTIFACTS]) put(dir, f, `${version}\n`); };
|
|
334
|
+
|
|
335
|
+
test('real git: two consecutive co-tenant upgrades each leave the tree clean, so the next pre-flight passes', () => {
|
|
336
|
+
const dir = scratchInstance();
|
|
337
|
+
for (const [from, to] of [['1.19.737', '1.19.745'], ['1.19.745', '1.19.746']]) {
|
|
338
|
+
assert.equal(gitTreeClean(dir).ok, true, `pre-flight before ${to}`);
|
|
339
|
+
upgradeWrites(dir, to);
|
|
340
|
+
const r = persistPin({ instanceDir: dir, fromVersion: from, toVersion: to, commitPin: true, push: false, artifactPaths: ARTIFACTS }, spawnSync, quiet, quiet);
|
|
341
|
+
assert.equal(r.committed, true, `upgrade to ${to} committed`);
|
|
342
|
+
assert.equal(git(dir, 'status', '--porcelain').stdout, '', `upgrade to ${to} leaves nothing uncommitted`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('real git: an owner edit outside the generated files is never swept into the pin commit', () => {
|
|
347
|
+
const dir = scratchInstance();
|
|
348
|
+
put(dir, 'config/branding.json', 'the owner was here\n');
|
|
349
|
+
upgradeWrites(dir, '1.19.745');
|
|
350
|
+
persistPin({ instanceDir: dir, fromVersion: '1.19.737', toVersion: '1.19.745', commitPin: true, push: false, artifactPaths: ARTIFACTS }, spawnSync, quiet, quiet);
|
|
351
|
+
assert.equal(git(dir, 'status', '--porcelain').stdout, ' M config/branding.json\n');
|
|
352
|
+
assert.ok(!git(dir, 'show', '--name-only', '--format=', 'HEAD').stdout.includes('config/branding.json'));
|
|
353
|
+
});
|