@bongos/core 1.19.748 → 1.19.750
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 +22 -17
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/gds-db-backup.md +1 -1
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/db-backup-nightly.sh +21 -7
- package/scripts/gds/upgrade.js +36 -12
- package/src/module-api.js +1 -1
- package/tests/db_backup_nightly.mjs +79 -0
- package/tests/upgrade.mjs +39 -1
- package/tests/upgrade_persist_pin.mjs +102 -1
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.750",
|
|
6
|
+
"core_contract": "1.19.750",
|
|
7
|
+
"source_commit": "70d84110fa87578e522d55cec0236a0719f25767",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-15T22:23:58.558Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 482,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2110,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2617,
|
|
20
|
+
"tree_sha256": "a8d1e331aab73c6da3b924d2defdcf6ac1d55f6a9ccfed12c7922e90314e7417",
|
|
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": "996dff9026c0402289d079b4a3cab9b437def3df20adbc465c841b5064ff1b48"
|
|
2511
2511
|
},
|
|
2512
2512
|
{
|
|
2513
2513
|
"path": "docs/modules-contract.md",
|
|
@@ -2652,7 +2652,7 @@
|
|
|
2652
2652
|
{
|
|
2653
2653
|
"path": "docs/recipes/gds-db-backup.md",
|
|
2654
2654
|
"mode": "0000644",
|
|
2655
|
-
"sha256": "
|
|
2655
|
+
"sha256": "0f2dfdeca8d49a07a0da043eaa9f2b16669228c358cb56a7786c6c59e5e29808"
|
|
2656
2656
|
},
|
|
2657
2657
|
{
|
|
2658
2658
|
"path": "docs/recipes/instance-deploy.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": "f0450a8f923e11ca0b92de0ead3f2b9b49a8c01f721be06ddd2566845a082775"
|
|
7566
7566
|
},
|
|
7567
7567
|
{
|
|
7568
7568
|
"path": "package.json",
|
|
7569
7569
|
"mode": "0000644",
|
|
7570
|
-
"sha256": "
|
|
7570
|
+
"sha256": "4bc87648a58a6f4b15ffe9f9bda38a0edace87d66e9e319317804d3f1ee347ac"
|
|
7571
7571
|
},
|
|
7572
7572
|
{
|
|
7573
7573
|
"path": "public-docs/index.html",
|
|
@@ -7912,7 +7912,7 @@
|
|
|
7912
7912
|
{
|
|
7913
7913
|
"path": "scripts/gds/db-backup-nightly.sh",
|
|
7914
7914
|
"mode": "0000644",
|
|
7915
|
-
"sha256": "
|
|
7915
|
+
"sha256": "ff4785dbde6a5c52036ce4c4aacca317c62e725f49d3e32201909ec444d85d44"
|
|
7916
7916
|
},
|
|
7917
7917
|
{
|
|
7918
7918
|
"path": "scripts/gds/db-backup-offsite.sh",
|
|
@@ -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": "de52088bfb2f473ad6960e60ee94153cdf1b74d4634ed4934d22e977988278a9"
|
|
9371
9371
|
},
|
|
9372
9372
|
{
|
|
9373
9373
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10224,6 +10224,11 @@
|
|
|
10224
10224
|
"mode": "0000644",
|
|
10225
10225
|
"sha256": "434130945232d2a6959281070e6dbee31697904b3455fb3fd09f198a64bb0e33"
|
|
10226
10226
|
},
|
|
10227
|
+
{
|
|
10228
|
+
"path": "tests/db_backup_nightly.mjs",
|
|
10229
|
+
"mode": "0000644",
|
|
10230
|
+
"sha256": "020dc551d105cc4a29e0f9c750a03cd14a91511d3edc47eb6719a1e430461936"
|
|
10231
|
+
},
|
|
10227
10232
|
{
|
|
10228
10233
|
"path": "tests/dead_deps.mjs",
|
|
10229
10234
|
"mode": "0000644",
|
|
@@ -12942,12 +12947,12 @@
|
|
|
12942
12947
|
{
|
|
12943
12948
|
"path": "tests/upgrade.mjs",
|
|
12944
12949
|
"mode": "0000644",
|
|
12945
|
-
"sha256": "
|
|
12950
|
+
"sha256": "bcbba7b1c952088f43300405184849bbcbb2da789097fcc4cf38d95441e0c158"
|
|
12946
12951
|
},
|
|
12947
12952
|
{
|
|
12948
12953
|
"path": "tests/upgrade_persist_pin.mjs",
|
|
12949
12954
|
"mode": "0000644",
|
|
12950
|
-
"sha256": "
|
|
12955
|
+
"sha256": "864b9471fcd6bfc41e99fd5356a3124c25d288946326c2797cc2bacb86120d05"
|
|
12951
12956
|
},
|
|
12952
12957
|
{
|
|
12953
12958
|
"path": "tests/verify_task_guard.mjs",
|
|
@@ -1970,5 +1970,9 @@ 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)
|
|
1975
|
+
1.19.750 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1976
|
+
landed since 1.19.749 with no explicit bump. run 35030717430. (task 1002620)
|
|
1973
1977
|
---------------------------------------------------------------------------
|
|
1974
1978
|
```
|
|
@@ -21,7 +21,7 @@ This closes the **most likely** loss-of-data risks: schema corruption, an accide
|
|
|
21
21
|
|
|
22
22
|
- **Filename format:** `<dbname>-<UTC-timestamp>.sql.gz`. UTC is used so the dumps sort chronologically regardless of the droplet's timezone.
|
|
23
23
|
- **Permissions:** dir is `lars:lars 0750`; files are `lars:lars 0640`. No root needed for routine inspection.
|
|
24
|
-
- **Size:** each gzipped dump is well under 1 MB, and 30 days of them fit comfortably — pruning is hygiene, not disk pressure. **The dump excludes `search_chunks` data** (the `/recall` lexical index, migration 103) via `--exclude-table-data=search_chunks` ([#973](https://example.com/builders#/task/973)): it is regenerable derived data — chunked copies of DB prose (already dumped) and repo markdown (already in git) — and dumping its rows roughly duplicated the prose corpus, making it the dump's dominant growth vector. The table schema + GIN indexes are still dumped, so a restore recreates it empty; the index rebuilds from source (see the restore drill below). A size guard in the backup script WARNs to journald if a dump ever exceeds `GDS_BACKUP_WARN_BYTES` (default 25 MiB) — a generous tripwire for unexpected growth from some *other* table.
|
|
24
|
+
- **Size:** each gzipped dump is well under 1 MB, and 30 days of them fit comfortably — pruning is hygiene, not disk pressure. **The dump excludes `search_chunks` data** (the `/recall` lexical index, migration 103) via `--exclude-table-data=search_chunks` ([#973](https://example.com/builders#/task/973)): it is regenerable derived data — chunked copies of DB prose (already dumped) and repo markdown (already in git) — and dumping its rows roughly duplicated the prose corpus, making it the dump's dominant growth vector. The table schema + GIN indexes are still dumped, so a restore recreates it empty; the index rebuilds from source (see the restore drill below). A size guard in the backup script WARNs to journald if a dump ever exceeds `GDS_BACKUP_WARN_BYTES` (default 25 MiB) — a generous tripwire for unexpected growth from some *other* table. A dump is refused (and deleted) only when it is **incomplete** — missing pg_dump's closing `dump complete` line — never for being small: a fresh project's whole database gzips to ~36 KB, and the fixed 1 MiB floor this replaced failed and deleted every project's nightly dump ([task 1003925](https://cloudbongos.com/builders#/task/1003925)). An optional extra size floor can still be set in the unit's environment (see the script).
|
|
25
25
|
|
|
26
26
|
## What's scheduled
|
|
27
27
|
|
|
@@ -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.750",
|
|
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.750",
|
|
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.750",
|
|
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",
|
|
@@ -130,13 +130,27 @@ mv "$OUT.partial" "$OUT"
|
|
|
130
130
|
BYTES=$(stat -c '%s' "$OUT")
|
|
131
131
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: wrote ${OUT} (${BYTES} bytes)"
|
|
132
132
|
|
|
133
|
-
#
|
|
134
|
-
# on a pg_dump error
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
# Completeness guard (incident 2026-07-02, task 1711; task 1003925). pipefail already
|
|
134
|
+
# aborts on a pg_dump error; this is belt-and-suspenders against a dump cut short. A
|
|
135
|
+
# plain-format dump always closes with pg_dump's own "dump complete" comment (patched
|
|
136
|
+
# servers append a \unrestrict line after it), whatever its size. The fixed 1 MiB floor
|
|
137
|
+
# this replaces was sized for the platform database and failed — and deleted — every
|
|
138
|
+
# valid dump of a small project (~36 KB gzipped). Fail loudly and delete a broken dump
|
|
139
|
+
# so it can never masquerade as a good one.
|
|
140
|
+
DUMP_TAIL=$(gzip -dc "$OUT" 2>/dev/null | tail -n 10) || DUMP_TAIL=''
|
|
141
|
+
FAIL_REASON=''
|
|
142
|
+
case "$DUMP_TAIL" in
|
|
143
|
+
*'-- PostgreSQL database dump complete'*) ;;
|
|
144
|
+
*) FAIL_REASON="(${BYTES} bytes) is incomplete — no pg_dump completion line" ;;
|
|
145
|
+
esac
|
|
146
|
+
|
|
147
|
+
# Optional operator floor. Unset means none: no single size fits every instance.
|
|
148
|
+
MIN_BYTES="${GDS_BACKUP_MIN_BYTES:-0}"
|
|
149
|
+
if [ -z "$FAIL_REASON" ] && [ "$BYTES" -lt "$MIN_BYTES" ]; then
|
|
150
|
+
FAIL_REASON="is only ${BYTES} bytes (< ${MIN_BYTES} floor)"
|
|
151
|
+
fi
|
|
152
|
+
if [ -n "$FAIL_REASON" ]; then
|
|
153
|
+
echo "[$(date -u +%FT%TZ)] gds-db-backup: ERROR latest dump ${FAIL_REASON} — treating as FAILED." >&2
|
|
140
154
|
rm -f "$OUT"
|
|
141
155
|
exit 1
|
|
142
156
|
fi
|
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.750'; // 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');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// tests/db_backup_nightly.mjs — the nightly dump's sanity guard (task 1003925).
|
|
2
|
+
//
|
|
3
|
+
// THE BUG. db-backup-nightly.sh refused any dump under a fixed 1 MiB floor, sized for the
|
|
4
|
+
// platform's own database. A small project's whole database gzips to ~36 KB, so every
|
|
5
|
+
// project's nightly unit failed AND the valid dump was deleted: on 2026-09-15 twelve backup
|
|
6
|
+
// units were failed and no project had a single retained dump. The guard now asks whether
|
|
7
|
+
// the dump is COMPLETE (pg_dump's closing comment), which is true of a valid dump at any size
|
|
8
|
+
// and false of one cut short.
|
|
9
|
+
//
|
|
10
|
+
// The script runs for real against a fake `pg_dump` on PATH, so nothing touches a database.
|
|
11
|
+
//
|
|
12
|
+
// Run: node --test tests/db_backup_nightly.mjs
|
|
13
|
+
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const SCRIPT = path.join(ROOT, 'scripts', 'gds', 'db-backup-nightly.sh');
|
|
24
|
+
const skip = process.platform === 'win32' ? 'needs a POSIX bash + coreutils (runs in CI)' : false;
|
|
25
|
+
// The script's env-var prefix, built rather than written so the legacy-literal ratchet
|
|
26
|
+
// does not count a test that has to speak the script's own variable names.
|
|
27
|
+
const ENV = 'gds_backup_'.toUpperCase();
|
|
28
|
+
|
|
29
|
+
// A fake pg_dump: a plain-format dump of `rows` rows, closed the way pg_dump closes one
|
|
30
|
+
// (including the \unrestrict line patched servers append) unless it is `cut` short, and
|
|
31
|
+
// exiting `exitCode`.
|
|
32
|
+
function runBackup({ rows = 40, cut = false, exitCode = 0, env = {} } = {}) {
|
|
33
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-'));
|
|
34
|
+
const bin = path.join(tmp, 'bin');
|
|
35
|
+
const dir = path.join(tmp, 'backups');
|
|
36
|
+
fs.mkdirSync(bin);
|
|
37
|
+
fs.mkdirSync(dir);
|
|
38
|
+
const body = Array.from({ length: rows }, (_, i) => `INSERT INTO public.t VALUES (${i});`).join('\n');
|
|
39
|
+
const close = cut ? '' : '\n--\n-- PostgreSQL database dump complete\n--\n\n\\unrestrict abc123\n';
|
|
40
|
+
fs.writeFileSync(path.join(bin, 'pg_dump'),
|
|
41
|
+
`#!/bin/bash\ncat <<'EOF'\n--\n-- PostgreSQL database dump\n--\n${body}\n${close}EOF\nexit ${exitCode}\n`, { mode: 0o755 });
|
|
42
|
+
const r = spawnSync('bash', [SCRIPT], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, [`${ENV}DB`]: 'smallproj', [`${ENV}DIR`]: dir, ...env },
|
|
45
|
+
});
|
|
46
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, dumps: fs.readdirSync(dir).filter((f) => f.endsWith('.sql.gz')) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('a complete dump of a small project is kept, however small', { skip }, () => {
|
|
50
|
+
const r = runBackup();
|
|
51
|
+
assert.equal(r.status, 0, r.stderr);
|
|
52
|
+
assert.equal(r.dumps.length, 1, 'the valid dump is retained');
|
|
53
|
+
assert.match(r.stdout, /gds-db-backup: ok/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a dump cut short fails and is removed, whatever its size', { skip }, () => {
|
|
57
|
+
const r = runBackup({ rows: 50000, cut: true });
|
|
58
|
+
assert.equal(r.status, 1);
|
|
59
|
+
assert.equal(r.dumps.length, 0, 'an incomplete dump never masquerades as a good one');
|
|
60
|
+
assert.match(r.stderr, /incomplete/i);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a pg_dump that fails mid-stream still fails the run (pipefail, task 1711)', { skip }, () => {
|
|
64
|
+
const r = runBackup({ cut: true, exitCode: 1 });
|
|
65
|
+
assert.notEqual(r.status, 0);
|
|
66
|
+
assert.equal(r.dumps.length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('an explicit operator size floor is still enforced', { skip }, () => {
|
|
70
|
+
const r = runBackup({ env: { [`${ENV}MIN_BYTES`]: String(10 * 1024 * 1024) } });
|
|
71
|
+
assert.equal(r.status, 1);
|
|
72
|
+
assert.equal(r.dumps.length, 0);
|
|
73
|
+
assert.match(r.stderr, /floor/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('no fixed size floor is baked in for every instance', () => {
|
|
77
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
78
|
+
assert.doesNotMatch(src, new RegExp(`${ENV}MIN_BYTES:-[1-9]`), 'a default floor fails every database smaller than it');
|
|
79
|
+
});
|
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
|
+
});
|