@bongos/core 1.21.147 → 1.21.148
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 +25 -15
- package/bin/bongos.js +5 -2
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/core-origin-follow.js +185 -0
- package/scripts/gds/core-pin-sync.js +31 -5
- package/src/module-api.js +1 -1
- package/tests/core_origin_follow.mjs +287 -0
- package/tests/core_pin_sync.mjs +7 -2
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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.148",
|
|
6
|
+
"core_contract": "1.21.148",
|
|
7
|
+
"source_commit": "96de3db7b01db9d1d1ea6de0020e2863549ace48",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-19T16:38:35.780Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 608,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3168,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3805,
|
|
20
|
+
"tree_sha256": "9aa795e140a79d60c77adddcf814eb815324f8778aa3e839f2eed0d221aba834",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
{
|
|
313
313
|
"path": "bin/bongos.js",
|
|
314
314
|
"mode": "0000755",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "10da188550dd74f74a8598b8815076cf098908196f366aad1f6ca39e16c68c33"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
@@ -2927,7 +2927,7 @@
|
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/module-api-changelog.md",
|
|
2929
2929
|
"mode": "0000644",
|
|
2930
|
-
"sha256": "
|
|
2930
|
+
"sha256": "8f019f26dca4f3a5b624cbabd6d28976deef91faab61279479708ab9585b3fc9"
|
|
2931
2931
|
},
|
|
2932
2932
|
{
|
|
2933
2933
|
"path": "docs/modules-contract.md",
|
|
@@ -10552,12 +10552,12 @@
|
|
|
10552
10552
|
{
|
|
10553
10553
|
"path": "package-lock.json",
|
|
10554
10554
|
"mode": "0000644",
|
|
10555
|
-
"sha256": "
|
|
10555
|
+
"sha256": "c5456eee2c0d7c56f4a86b537ad4a056d7ee25178017ce8b2c4a6d584a700b91"
|
|
10556
10556
|
},
|
|
10557
10557
|
{
|
|
10558
10558
|
"path": "package.json",
|
|
10559
10559
|
"mode": "0000644",
|
|
10560
|
-
"sha256": "
|
|
10560
|
+
"sha256": "eeee21b5c79606fce2ec960818bbca2bda46e8487e821e086db727725dd34483"
|
|
10561
10561
|
},
|
|
10562
10562
|
{
|
|
10563
10563
|
"path": "public-docs/index.html",
|
|
@@ -10577,7 +10577,7 @@
|
|
|
10577
10577
|
{
|
|
10578
10578
|
"path": "release-notes.json",
|
|
10579
10579
|
"mode": "0000644",
|
|
10580
|
-
"sha256": "
|
|
10580
|
+
"sha256": "e75ce28a56ae19d8e908bca8b07e539fcb4066b72fb7e6f282e8dff5803f22b4"
|
|
10581
10581
|
},
|
|
10582
10582
|
{
|
|
10583
10583
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10954,10 +10954,15 @@
|
|
|
10954
10954
|
"mode": "0000644",
|
|
10955
10955
|
"sha256": "9dd80d2c37602c59cb75db0be02cfadc737fc64891a81d03f5be5f0ea5e35a27"
|
|
10956
10956
|
},
|
|
10957
|
+
{
|
|
10958
|
+
"path": "scripts/gds/core-origin-follow.js",
|
|
10959
|
+
"mode": "0000644",
|
|
10960
|
+
"sha256": "5d12058796dc7373584fc8447ee83b8da0820a447e783b2462156f1ec158b8c5"
|
|
10961
|
+
},
|
|
10957
10962
|
{
|
|
10958
10963
|
"path": "scripts/gds/core-pin-sync.js",
|
|
10959
10964
|
"mode": "0000644",
|
|
10960
|
-
"sha256": "
|
|
10965
|
+
"sha256": "9e2e6dd476938df51bf027396df8a92e0ed2ca96a9adf59cb666ad2681cc721e"
|
|
10961
10966
|
},
|
|
10962
10967
|
{
|
|
10963
10968
|
"path": "scripts/gds/coreB-carve-proof.sh",
|
|
@@ -12952,7 +12957,7 @@
|
|
|
12952
12957
|
{
|
|
12953
12958
|
"path": "src/module-api.js",
|
|
12954
12959
|
"mode": "0000644",
|
|
12955
|
-
"sha256": "
|
|
12960
|
+
"sha256": "2b61d02ebd3b94b723ccec869d7d6432ce9c035c5135259cf6e709759622f0af"
|
|
12956
12961
|
},
|
|
12957
12962
|
{
|
|
12958
12963
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13974,10 +13979,15 @@
|
|
|
13974
13979
|
"mode": "0000644",
|
|
13975
13980
|
"sha256": "4612301c02a9a6f9927a49f76a2b49d2892e8266580cf31bcd47dd5b36ad71b0"
|
|
13976
13981
|
},
|
|
13982
|
+
{
|
|
13983
|
+
"path": "tests/core_origin_follow.mjs",
|
|
13984
|
+
"mode": "0000644",
|
|
13985
|
+
"sha256": "1b136186f5d91fe9f63034559115ba505959a742712e4103a143a7ede0917869"
|
|
13986
|
+
},
|
|
13977
13987
|
{
|
|
13978
13988
|
"path": "tests/core_pin_sync.mjs",
|
|
13979
13989
|
"mode": "0000644",
|
|
13980
|
-
"sha256": "
|
|
13990
|
+
"sha256": "34d78bb4998adaec12f5666d4ac58815ebceaccff1d560ca978c58ba64190575"
|
|
13981
13991
|
},
|
|
13982
13992
|
{
|
|
13983
13993
|
"path": "tests/core_update_banner.mjs",
|
package/bin/bongos.js
CHANGED
|
@@ -195,8 +195,11 @@ function main(argv) {
|
|
|
195
195
|
// of its own, so after a core update lands it resolves the main checkout's older core
|
|
196
196
|
// and the ship fails on a bug the pin already fixed. `ship` installs the pinned core
|
|
197
197
|
// into the checkout and re-runs under it; scripts/gds/core-pin-sync.js.
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// First, both `ship` and `start` (an instance's session start) follow a core move the
|
|
199
|
+
// hub pushed to GitHub: one line saying so, and on a clean main a fast-forward plus
|
|
200
|
+
// install (task 1004732; scripts/gds/core-origin-follow.js).
|
|
201
|
+
if (!COMPILED && (verb === 'ship' || verb === 'start')) {
|
|
202
|
+
const synced = require('../scripts/gds/core-origin-follow.js').ensureCurrentCore({ coreRoot: REPO_ROOT, argv, verb });
|
|
200
203
|
if (synced != null) return synced;
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -3131,5 +3131,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3131
3131
|
landed since 1.21.145 with no explicit bump. run 39077391288. (task 1002620)
|
|
3132
3132
|
1.21.147 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3133
3133
|
landed since 1.21.146 with no explicit bump. run 39274828902. (task 1002620)
|
|
3134
|
+
1.21.148 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3135
|
+
landed since 1.21.147 with no explicit bump. run 39286989989. (task 1002620)
|
|
3134
3136
|
---------------------------------------------------------------------------
|
|
3135
3137
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.148",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.148",
|
|
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.21.
|
|
3
|
+
"version": "1.21.148",
|
|
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/release-notes.json
CHANGED
|
@@ -9896,5 +9896,11 @@
|
|
|
9896
9896
|
"id": "1004731",
|
|
9897
9897
|
"text": "After a project's software is updated, its owner now sees a one-time note saying which version it moved to, when, and what changed, and the Software update page lists the recent updates, including any that failed and were undo"
|
|
9898
9898
|
}
|
|
9899
|
+
],
|
|
9900
|
+
"1.21.148": [
|
|
9901
|
+
{
|
|
9902
|
+
"id": "1004732",
|
|
9903
|
+
"text": "When the hub upgrades a project's core, your laptop now notices at the next session start or ship, says so in one line, and on a clean main catches up and reinstalls the new core by itself."
|
|
9904
|
+
}
|
|
9899
9905
|
]
|
|
9900
9906
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// scripts/gds/core-origin-follow.js — the laptop follows a core move the hub made (task 1004732)
|
|
2
|
+
//
|
|
3
|
+
// WHY. When the hub moves a hosted project's core (provision-core-upgrade.js), it commits
|
|
4
|
+
// the new pin — package.json + lockfile, usually a vendored `file:vendor/bongos-core-X.tgz`
|
|
5
|
+
// rewritten to an exact registry version — on the box and pushes it to the project's
|
|
6
|
+
// GitHub repo. The owner's laptop checkout never hears of it: the next session runs the
|
|
7
|
+
// old core, and the next ship merges against the old pin. Seen on toast (1.21.20 on the
|
|
8
|
+
// laptop while fixes sat in newer releases).
|
|
9
|
+
//
|
|
10
|
+
// THE FIX. Before `bongos start` (an instance's session start: its CLAUDE.md opens every
|
|
11
|
+
// session with it, and a package-installed instance carries no SessionStart hooks) and
|
|
12
|
+
// before `bongos ship`, the dispatcher calls ensureCurrentCore(). It fetches origin/main
|
|
13
|
+
// (bounded; allowed to fail) and compares the checkout's @bongos/core pin with
|
|
14
|
+
// origin/main's. When ORIGIN moved it, the builder gets one plain line saying so. On a
|
|
15
|
+
// clean `main` with no local commits, the checkout is fast-forwarded and the pinned core
|
|
16
|
+
// is installed through core-pin-sync.js (task 1004632) — whose gates (the pin's shape,
|
|
17
|
+
// and the pin having landed on main) still decide what may be installed — then the
|
|
18
|
+
// command re-runs under the new core. A dirty tree, a feature branch or local commits get
|
|
19
|
+
// the line and the command to run, never a forced change. So does a move that is not
|
|
20
|
+
// pin-only (notPinOnly): the fast-forward installs and runs what it brings in, so it is
|
|
21
|
+
// taken only for the hub's commit shape — the pin, the lockfile, a removed tarball — onto
|
|
22
|
+
// a published registry release; anything wider waits for the owner to look.
|
|
23
|
+
//
|
|
24
|
+
// Silent and fail-open: in the core repo itself (the running package is not
|
|
25
|
+
// @bongos/core) it returns before any git spawn; outside git, with no pin, when origin
|
|
26
|
+
// cannot be read, or when the pins already agree it says nothing. A pin the LOCAL side
|
|
27
|
+
// moved (origin's pin is still the one this branch forked from) is not a hub move.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { spawnSync } = require('node:child_process');
|
|
34
|
+
const pinSync = require('./core-pin-sync.js');
|
|
35
|
+
|
|
36
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
37
|
+
|
|
38
|
+
function readJson(file, fsImpl = fs) {
|
|
39
|
+
try { return JSON.parse(String(fsImpl.readFileSync(file, 'utf8')).replace(/^/, '')); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gitOut(args, cwd, run, extra = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const r = run('git', args, { cwd, encoding: 'utf8', ...extra });
|
|
45
|
+
return r && r.status === 0 ? String(r.stdout || '').trim() : null;
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function specOf(pkg) {
|
|
50
|
+
if (!pkg) return null;
|
|
51
|
+
return (pkg.dependencies || {})[pinSync.CORE_PKG] || (pkg.devDependencies || {})[pinSync.CORE_PKG] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The core pin package.json carries at `rev`, or null when that file cannot be read there.
|
|
55
|
+
function specAt(root, rev, run) {
|
|
56
|
+
const body = gitOut(['show', `${rev}:package.json`], root, run, { maxBuffer: 8 * 1024 * 1024 });
|
|
57
|
+
if (body == null) return null;
|
|
58
|
+
try { return specOf(JSON.parse(body.replace(/^/, ''))); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shown = (spec) => pinSync.versionFromSpec(spec) || spec;
|
|
62
|
+
|
|
63
|
+
// Why the move cannot be applied to this checkout automatically, or null when it can.
|
|
64
|
+
function blockedReason({ branch, dirty, ahead }) {
|
|
65
|
+
if (!branch) return 'this checkout is not on a branch';
|
|
66
|
+
if (branch !== 'main') return `you are on branch ${branch}, which picks the move up when it merges main`;
|
|
67
|
+
if (dirty == null || dirty !== '') return 'this checkout has uncommitted changes';
|
|
68
|
+
if (!Number.isFinite(ahead) || ahead !== 0) return 'main has local commits GitHub does not have';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const REMEDY = 'On a clean main: `git pull --ff-only origin main`, then `npm ci`.';
|
|
73
|
+
const REVIEW_REMEDY = 'Review what GitHub has (`git log HEAD..origin/main`), then `git pull --ff-only origin main` and `npm ci`.';
|
|
74
|
+
const REGISTRY = 'https://registry.npmjs.org/';
|
|
75
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?$/;
|
|
76
|
+
|
|
77
|
+
function jsonAt(root, rev, file, run) {
|
|
78
|
+
const body = gitOut(['show', `${rev}:${file}`], root, run, { maxBuffer: 64 * 1024 * 1024 });
|
|
79
|
+
if (body == null) return null;
|
|
80
|
+
try { return JSON.parse(body.replace(/^/, '')); } catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The fast-forward installs and then RUNS what it brings in, unattended — so it is taken
|
|
84
|
+
// only when what GitHub has is the hub's pin move and nothing else (the shape
|
|
85
|
+
// provision-core-upgrade.js pushes). Anything wider is reported, never applied. Returns
|
|
86
|
+
// null when the move is pin-only, else the reason it is not.
|
|
87
|
+
// - the only files that differ are package.json, package-lock.json and a removed
|
|
88
|
+
// vendored tarball;
|
|
89
|
+
// - package.json differs ONLY in the @bongos/core spec;
|
|
90
|
+
// - the new spec is an exact version, i.e. a published release (a tarball brought in
|
|
91
|
+
// from GitHub is code nobody released);
|
|
92
|
+
// - the lockfile pins that version from the public registry, and every lockfile
|
|
93
|
+
// source it adds is the public registry too.
|
|
94
|
+
function notPinOnly(root, run) {
|
|
95
|
+
const diff = gitOut(['diff', '--name-status', '--no-renames', 'HEAD', 'origin/main'], root, run, { maxBuffer: 16 * 1024 * 1024 });
|
|
96
|
+
if (diff == null) return 'the change on GitHub could not be read';
|
|
97
|
+
for (const line of diff.split('\n').filter(Boolean)) {
|
|
98
|
+
const [status, file] = line.split('\t');
|
|
99
|
+
const ok = file === 'package.json' || file === 'package-lock.json' ||
|
|
100
|
+
(status === 'D' && /^vendor\/bongos-core-[^/]+\.tgz$/.test(file));
|
|
101
|
+
if (!ok) return 'GitHub has other changes besides the core move';
|
|
102
|
+
}
|
|
103
|
+
const before = jsonAt(root, 'HEAD', 'package.json', run);
|
|
104
|
+
const after = jsonAt(root, 'origin/main', 'package.json', run);
|
|
105
|
+
if (!before || !after) return 'package.json could not be read';
|
|
106
|
+
const spec = specOf(after);
|
|
107
|
+
const strip = (pkg) => {
|
|
108
|
+
const c = JSON.parse(JSON.stringify(pkg));
|
|
109
|
+
for (const k of ['dependencies', 'devDependencies']) if (c[k]) delete c[k][pinSync.CORE_PKG];
|
|
110
|
+
return JSON.stringify(c);
|
|
111
|
+
};
|
|
112
|
+
if (strip(before) !== strip(after)) return 'package.json changed beyond the core pin';
|
|
113
|
+
if (!EXACT_SEMVER.test(String(spec || ''))) return 'the new pin is not a published release';
|
|
114
|
+
const lock = jsonAt(root, 'origin/main', 'package-lock.json', run);
|
|
115
|
+
const entry = lock && lock.packages && lock.packages[`node_modules/${pinSync.CORE_PKG}`];
|
|
116
|
+
if (!entry || entry.version !== spec || !String(entry.resolved || '').startsWith(`${REGISTRY}${pinSync.CORE_PKG}/-/`)) {
|
|
117
|
+
return 'the lockfile does not pin that release from the public registry';
|
|
118
|
+
}
|
|
119
|
+
const prior = new Set(Object.values(((jsonAt(root, 'HEAD', 'package-lock.json', run) || {}).packages) || {}).map((p) => p && p.resolved));
|
|
120
|
+
for (const p of Object.values(lock.packages)) {
|
|
121
|
+
const r = p && p.resolved;
|
|
122
|
+
if (r && !prior.has(r) && !String(r).startsWith(REGISTRY)) return 'the lockfile adds a source outside the public registry';
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Returns { action: 'none' | 'reported' | 'fast-forwarded', reason, from?, to? }.
|
|
128
|
+
function followOriginPin({ coreRoot, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, fetchTimeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
129
|
+
const none = (reason) => ({ action: 'none', reason });
|
|
130
|
+
if (env && (env.BONGOS_SKIP_CORE_SYNC || env[pinSync.SYNCED_ENV])) return none('skipped');
|
|
131
|
+
const running = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
132
|
+
if (!running || running.name !== pinSync.CORE_PKG) return none('core-repo'); // no git spawn
|
|
133
|
+
const top = gitOut(['rev-parse', '--show-toplevel'], cwd, run);
|
|
134
|
+
if (!top) return none('no-git');
|
|
135
|
+
const root = path.resolve(top);
|
|
136
|
+
const local = specOf(readJson(path.join(root, 'package.json'), fsImpl));
|
|
137
|
+
if (!local) return none('no-pin');
|
|
138
|
+
|
|
139
|
+
// Bounded, quiet, never prompts; a failure is fine — the ref below decides.
|
|
140
|
+
gitOut(['fetch', '--quiet', 'origin', 'main'], root, run, {
|
|
141
|
+
timeout: fetchTimeoutMs, env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: '0' },
|
|
142
|
+
});
|
|
143
|
+
const origin = specAt(root, 'origin/main', run);
|
|
144
|
+
if (!origin) return none('origin-unreadable');
|
|
145
|
+
if (origin === local) return none('in-step');
|
|
146
|
+
const forkPoint = gitOut(['merge-base', 'HEAD', 'origin/main'], root, run);
|
|
147
|
+
if (forkPoint && specAt(root, forkPoint, run) === origin) return none('local-change');
|
|
148
|
+
|
|
149
|
+
const say = log || ((s) => process.stderr.write(s + '\n'));
|
|
150
|
+
const move = { from: local, to: origin };
|
|
151
|
+
say(`bongos: this project was upgraded to core ${shown(origin)} on GitHub (this checkout has ${shown(local)}).`);
|
|
152
|
+
const why = blockedReason({
|
|
153
|
+
branch: gitOut(['symbolic-ref', '--short', '-q', 'HEAD'], root, run),
|
|
154
|
+
dirty: gitOut(['status', '--porcelain', '--untracked-files=no'], root, run),
|
|
155
|
+
ahead: Number(gitOut(['rev-list', '--count', 'origin/main..HEAD'], root, run)),
|
|
156
|
+
});
|
|
157
|
+
if (why) {
|
|
158
|
+
say(` Not updated here: ${why}. ${REMEDY}`);
|
|
159
|
+
return { action: 'reported', reason: why, ...move };
|
|
160
|
+
}
|
|
161
|
+
const wider = notPinOnly(root, run);
|
|
162
|
+
if (wider) {
|
|
163
|
+
say(` Not updated automatically: ${wider}. ${REVIEW_REMEDY}`);
|
|
164
|
+
return { action: 'reported', reason: wider, ...move };
|
|
165
|
+
}
|
|
166
|
+
if (gitOut(['merge', '--ff-only', '--quiet', 'origin/main'], root, run) == null) {
|
|
167
|
+
say(` The fast-forward did not go through. ${REMEDY}`);
|
|
168
|
+
return { action: 'reported', reason: 'ff-failed', ...move };
|
|
169
|
+
}
|
|
170
|
+
say(' Fast-forwarded this checkout to it; installing that core next.');
|
|
171
|
+
return { action: 'fast-forwarded', reason: 'origin-moved', ...move };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The dispatcher's one call, for `ship` and `start`. Returns an exit code when it handled
|
|
175
|
+
// the command (installed and re-ran, or refused), or null to run the verb as it is.
|
|
176
|
+
// `ship` always checks the running core against the pin (task 1004632); `start` only
|
|
177
|
+
// after a fast-forward actually moved the pin, so a normal session start pays no more
|
|
178
|
+
// than the fetch.
|
|
179
|
+
function ensureCurrentCore({ coreRoot, argv, verb, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
180
|
+
const followed = followOriginPin({ coreRoot, cwd, env, run, fsImpl, log });
|
|
181
|
+
if (verb !== 'ship' && followed.action !== 'fast-forwarded') return null;
|
|
182
|
+
return pinSync.ensurePinnedCore({ coreRoot, argv, cwd, env, run, fsImpl, log, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { followOriginPin, ensureCurrentCore };
|
|
@@ -172,6 +172,28 @@ function unlinkLink(linkPath, { platform = process.platform, fsImpl = fs } = {})
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// A vendored instance keeps an untracked `scripts -> node_modules/@bongos/core/scripts`
|
|
176
|
+
// link in its main checkout (grading needs it; task 1004728). After an install it must
|
|
177
|
+
// still resolve (task 1004732): a link left dangling is re-pointed at the installed core.
|
|
178
|
+
// Only ever a LINK — a real scripts/ directory (the core repo) is never touched.
|
|
179
|
+
// Returns 'absent' | 'not-a-link' | 'ok' | 'repointed' | 'unresolved'.
|
|
180
|
+
function healScriptsLink(root, { fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), platform = process.platform } = {}) {
|
|
181
|
+
const link = path.join(root, 'scripts');
|
|
182
|
+
let st = null;
|
|
183
|
+
try { st = fsImpl.lstatSync(link); } catch { return 'absent'; }
|
|
184
|
+
if (!st.isSymbolicLink()) return 'not-a-link';
|
|
185
|
+
if (fsImpl.existsSync(path.join(link, 'gds'))) return 'ok';
|
|
186
|
+
const target = path.join(root, 'node_modules', '@bongos', 'core', 'scripts');
|
|
187
|
+
const fix = `bongos: the \`scripts\` link in ${root} no longer resolves; point it at ${target}.`;
|
|
188
|
+
if (!fsImpl.existsSync(target)) { log(fix); return 'unresolved'; }
|
|
189
|
+
try {
|
|
190
|
+
unlinkLink(link, { platform, fsImpl });
|
|
191
|
+
fsImpl.symlinkSync(target, link, 'junction');
|
|
192
|
+
} catch { log(fix); return 'unresolved'; }
|
|
193
|
+
log('bongos: re-pointed the `scripts` link at the installed core.');
|
|
194
|
+
return 'repointed';
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
// Install the pinned core into `root` and re-run the same bongos command under it.
|
|
176
198
|
// Returns the exit code to use.
|
|
177
199
|
function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), env = process.env, platform = process.platform, npmCli = npmCliPath(process.execPath, fsImpl) } = {}) {
|
|
@@ -192,10 +214,13 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
192
214
|
// npm's JS entry under this node — no shell. Fallback: `npm`, which on win32 is
|
|
193
215
|
// npm.cmd and needs a shell (Node refuses a .cmd without one); the args are fixed
|
|
194
216
|
// strings, never branch data.
|
|
217
|
+
// npm's output goes to stderr (task 1004732): `bongos start --widget` hands stdout to
|
|
218
|
+
// the renderer verbatim, so an install must never print there.
|
|
195
219
|
const cli = npmCli;
|
|
220
|
+
const stdio = ['ignore', 2, 2];
|
|
196
221
|
const inst = cli
|
|
197
|
-
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio
|
|
198
|
-
: run('npm', installArgs(hasLock), { cwd: root, stdio
|
|
222
|
+
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio, env })
|
|
223
|
+
: run('npm', installArgs(hasLock), { cwd: root, stdio, env, ...(platform === 'win32' ? { shell: true } : {}) });
|
|
199
224
|
if (!inst || inst.error || inst.status !== 0) {
|
|
200
225
|
log(`bongos: installing core ${pinned} failed.`);
|
|
201
226
|
remedy();
|
|
@@ -203,6 +228,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
203
228
|
}
|
|
204
229
|
const bin = path.join(nm, '@bongos', 'core', 'bin', 'bongos.js');
|
|
205
230
|
if (!fsImpl.existsSync(bin)) { remedy(); return 1; }
|
|
231
|
+
healScriptsLink(root, { fsImpl, log, platform });
|
|
206
232
|
const res = run(process.execPath, [bin, ...argv.slice(2)], { stdio: 'inherit', env: { ...env, [SYNCED_ENV]: '1' } });
|
|
207
233
|
if (!res || res.error) return 1;
|
|
208
234
|
return res.status == null ? 1 : res.status;
|
|
@@ -210,7 +236,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
210
236
|
|
|
211
237
|
// The dispatcher's one call. Returns an exit code when it handled the command (synced
|
|
212
238
|
// and re-ran, or refused), or null to run the verb on the current core.
|
|
213
|
-
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log } = {}) {
|
|
239
|
+
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
214
240
|
if (env && env.BONGOS_SKIP_CORE_SYNC) return null;
|
|
215
241
|
const runningPkg = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
216
242
|
if (!runningPkg || runningPkg.name !== CORE_PKG) return null; // fast path: the core repo itself
|
|
@@ -227,7 +253,7 @@ function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.e
|
|
|
227
253
|
say(remedyLine({ ...plan, hasLock: fsImpl.existsSync(path.join(root, 'package-lock.json')) }));
|
|
228
254
|
return 1;
|
|
229
255
|
}
|
|
230
|
-
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env });
|
|
256
|
+
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
231
257
|
}
|
|
232
258
|
|
|
233
|
-
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, ensurePinnedCore };
|
|
259
|
+
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, healScriptsLink, ensurePinnedCore };
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.148'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// tests/core_origin_follow.mjs — the laptop picks up a core move the hub made (task 1004732)
|
|
2
|
+
//
|
|
3
|
+
// The bug: when the hub moves a hosted project's core it commits the new pin on the box
|
|
4
|
+
// and pushes it to the project's GitHub repo. The owner's laptop checkout never heard of
|
|
5
|
+
// it, so the next session and the next ship ran — and merged against — the old pin.
|
|
6
|
+
//
|
|
7
|
+
// Each case is a REAL temp git setup: a bare "GitHub" origin, a "box" clone that makes
|
|
8
|
+
// the hub's pin commit and pushes it, and a "laptop" clone that is the checkout under
|
|
9
|
+
// test. npm is stubbed (it lays down a fake installed core); git is real.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/core_origin_follow.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const follow = require('../scripts/gds/core-origin-follow.js');
|
|
22
|
+
const pinSync = require('../scripts/gds/core-pin-sync.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function git(cwd, ...args) {
|
|
32
|
+
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8' });
|
|
33
|
+
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
34
|
+
return r.stdout;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const OLD = '1.21.20';
|
|
38
|
+
const NEW = '1.21.115';
|
|
39
|
+
|
|
40
|
+
function writePin(dir, spec, version) {
|
|
41
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'toast', private: true, dependencies: { '@bongos/core': spec } }, null, 2) + '\n');
|
|
42
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({
|
|
43
|
+
name: 'toast', lockfileVersion: 3,
|
|
44
|
+
packages: { 'node_modules/@bongos/core': { version, resolved: spec.startsWith('file:') ? spec : `https://registry.npmjs.org/@bongos/core/-/core-${version}.tgz` } },
|
|
45
|
+
}, null, 2) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A fake installed core in `root`: what npm would leave behind.
|
|
49
|
+
function installCore(root, version) {
|
|
50
|
+
const core = path.join(root, 'node_modules', '@bongos', 'core');
|
|
51
|
+
fs.rmSync(path.join(root, 'node_modules'), { recursive: true, force: true });
|
|
52
|
+
fs.mkdirSync(path.join(core, 'bin'), { recursive: true });
|
|
53
|
+
fs.mkdirSync(path.join(core, 'scripts', 'gds'), { recursive: true });
|
|
54
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({ name: '@bongos/core', version }));
|
|
55
|
+
fs.writeFileSync(path.join(core, 'bin', 'bongos.js'), '');
|
|
56
|
+
fs.writeFileSync(path.join(core, 'scripts', 'gds', 'fitness.js'), '');
|
|
57
|
+
return core;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// origin (bare) ← box pushes the hub's move; laptop is a clone made BEFORE the move.
|
|
61
|
+
// `move` makes the hub's commit: vendored tgz pin → exact registry pin, tgz removed.
|
|
62
|
+
function setup({ move = true, alsoOnBox = null } = {}) {
|
|
63
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-'));
|
|
64
|
+
const origin = path.join(base, 'origin.git');
|
|
65
|
+
const box = path.join(base, 'box');
|
|
66
|
+
const laptop = path.join(base, 'laptop');
|
|
67
|
+
git(base, 'init', '-q', '--bare', '-b', 'main', origin);
|
|
68
|
+
fs.mkdirSync(box);
|
|
69
|
+
git(box, 'init', '-q', '-b', 'main');
|
|
70
|
+
writePin(box, `file:vendor/bongos-core-${OLD}.tgz`, OLD);
|
|
71
|
+
fs.mkdirSync(path.join(box, 'vendor'));
|
|
72
|
+
fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${OLD}.tgz`), 'old-tarball');
|
|
73
|
+
fs.writeFileSync(path.join(box, '.gitignore'), 'node_modules/\n');
|
|
74
|
+
git(box, 'add', '-A');
|
|
75
|
+
git(box, 'commit', '-qm', 'init: vendored core');
|
|
76
|
+
git(box, 'remote', 'add', 'origin', origin);
|
|
77
|
+
git(box, 'push', '-q', 'origin', 'main');
|
|
78
|
+
git(base, 'clone', '-q', origin, laptop);
|
|
79
|
+
// The laptop has the old core installed and the untracked, excluded scripts link.
|
|
80
|
+
installCore(laptop, OLD);
|
|
81
|
+
fs.symlinkSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'scripts'), path.join(laptop, 'scripts'), 'junction');
|
|
82
|
+
fs.appendFileSync(path.join(laptop, '.git', 'info', 'exclude'), 'scripts\n');
|
|
83
|
+
if (move) {
|
|
84
|
+
writePin(box, NEW, NEW);
|
|
85
|
+
git(box, 'rm', '-q', `vendor/bongos-core-${OLD}.tgz`);
|
|
86
|
+
if (alsoOnBox) alsoOnBox(box);
|
|
87
|
+
git(box, 'add', '-A');
|
|
88
|
+
git(box, 'commit', '-qm', `core: upgrade @bongos/core to ${NEW}`);
|
|
89
|
+
git(box, 'push', '-q', 'origin', 'main');
|
|
90
|
+
}
|
|
91
|
+
// The core the laptop runs (node_modules/@bongos/core) — the dispatcher's REPO_ROOT.
|
|
92
|
+
const coreRoot = path.join(laptop, 'node_modules', '@bongos', 'core');
|
|
93
|
+
return { base, origin, box, laptop, coreRoot };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// git → real; npm → lay down the pinned core; the re-run of bongos → recorded.
|
|
97
|
+
function stubRun(root) {
|
|
98
|
+
const calls = { installs: [], reruns: [], git: 0 };
|
|
99
|
+
const run = (cmd, args, opts) => {
|
|
100
|
+
if (cmd === 'git') { calls.git++; return spawnSync(cmd, args, opts); }
|
|
101
|
+
if (cmd === 'npm' || String(args[0] || '').endsWith('npm-cli.js')) {
|
|
102
|
+
calls.installs.push(cmd === 'npm' ? args : args.slice(1));
|
|
103
|
+
const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
|
|
104
|
+
installCore(root, lock.packages['node_modules/@bongos/core'].version);
|
|
105
|
+
return { status: 0 };
|
|
106
|
+
}
|
|
107
|
+
if (String(args[0] || '').endsWith('bongos.js')) { calls.reruns.push(args.slice(1)); return { status: 0 }; }
|
|
108
|
+
return { status: 1 };
|
|
109
|
+
};
|
|
110
|
+
return { run, calls };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pinOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'];
|
|
114
|
+
const head = (dir) => git(dir, 'rev-parse', 'HEAD').trim();
|
|
115
|
+
|
|
116
|
+
test('clean main: says the project moved, fast-forwards, reinstalls, re-runs, scripts link resolves', () => {
|
|
117
|
+
const { laptop, coreRoot } = setup();
|
|
118
|
+
const { run, calls } = stubRun(laptop);
|
|
119
|
+
const logs = [];
|
|
120
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
121
|
+
assert.equal(code, 0, 'the re-run exit code: ' + logs.join('\n'));
|
|
122
|
+
assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`) && l.includes(OLD)), logs.join('\n'));
|
|
123
|
+
assert.equal(pinOf(laptop), NEW, 'fast-forwarded onto the hub commit');
|
|
124
|
+
assert.equal(calls.installs.length, 1, 'npm ran once');
|
|
125
|
+
assert.equal(calls.installs[0][0], 'ci');
|
|
126
|
+
assert.deepEqual(calls.reruns, [['start']], 'start re-ran under the new core');
|
|
127
|
+
assert.ok(fs.existsSync(path.join(laptop, 'scripts', 'gds', 'fitness.js')), 'the scripts link still resolves');
|
|
128
|
+
const installed = JSON.parse(fs.readFileSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'package.json'), 'utf8'));
|
|
129
|
+
assert.equal(installed.version, NEW);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('vendored → registry pin: no tgz left referenced after the follow', () => {
|
|
133
|
+
const { laptop, coreRoot } = setup();
|
|
134
|
+
const { run } = stubRun(laptop);
|
|
135
|
+
follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'ship', '7'], verb: 'ship', cwd: laptop, env: {}, run, log: () => {}, npmCli: null });
|
|
136
|
+
assert.equal(pinOf(laptop), NEW);
|
|
137
|
+
for (const f of ['package.json', 'package-lock.json']) {
|
|
138
|
+
assert.ok(!/bongos-core-[\d.]+\.tgz/.test(fs.readFileSync(path.join(laptop, f), 'utf8')), `${f} still names a tarball`);
|
|
139
|
+
}
|
|
140
|
+
assert.ok(!fs.existsSync(path.join(laptop, 'vendor', `bongos-core-${OLD}.tgz`)), 'the hub removed the old tarball; the follow carried that');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('dirty tree: reports the move only — no fast-forward, no install', () => {
|
|
144
|
+
const { laptop, coreRoot } = setup();
|
|
145
|
+
fs.writeFileSync(path.join(laptop, '.gitignore'), 'node_modules/\nlocal-edit\n');
|
|
146
|
+
const before = head(laptop);
|
|
147
|
+
const { run, calls } = stubRun(laptop);
|
|
148
|
+
const logs = [];
|
|
149
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
150
|
+
assert.equal(code, null, 'carry on with the command');
|
|
151
|
+
assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`)), logs.join('\n'));
|
|
152
|
+
assert.ok(logs.some((l) => /uncommitted/.test(l)), 'says why it was not applied: ' + logs.join('\n'));
|
|
153
|
+
assert.equal(head(laptop), before);
|
|
154
|
+
assert.equal(pinOf(laptop), `file:vendor/bongos-core-${OLD}.tgz`);
|
|
155
|
+
assert.equal(calls.installs.length, 0);
|
|
156
|
+
assert.match(fs.readFileSync(path.join(laptop, '.gitignore'), 'utf8'), /local-edit/, 'the edit is untouched');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('feature branch: reports only, never moves the branch', () => {
|
|
160
|
+
const { laptop, coreRoot } = setup();
|
|
161
|
+
git(laptop, 'checkout', '-qb', 'task-49');
|
|
162
|
+
const before = head(laptop);
|
|
163
|
+
const { run, calls } = stubRun(laptop);
|
|
164
|
+
const logs = [];
|
|
165
|
+
const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
|
|
166
|
+
assert.equal(r.action, 'reported');
|
|
167
|
+
assert.ok(logs.some((l) => l.includes('task-49')), logs.join('\n'));
|
|
168
|
+
assert.equal(head(laptop), before);
|
|
169
|
+
assert.equal(calls.installs.length, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The fast-forward installs and RUNS what it brings in, so only the hub's pin-only commit
|
|
173
|
+
// shape onto a published release is applied unattended; anything wider is reported.
|
|
174
|
+
function expectReportedOnly(alsoOnBox, why) {
|
|
175
|
+
const { laptop, coreRoot } = setup({ alsoOnBox });
|
|
176
|
+
const before = head(laptop);
|
|
177
|
+
const { run, calls } = stubRun(laptop);
|
|
178
|
+
const logs = [];
|
|
179
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
180
|
+
assert.equal(code, null);
|
|
181
|
+
assert.ok(logs.some((l) => l.includes('upgraded to core') ), logs.join('\n'));
|
|
182
|
+
assert.ok(logs.some((l) => why.test(l)), `names why (${why}): ` + logs.join('\n'));
|
|
183
|
+
assert.equal(head(laptop), before, 'not fast-forwarded');
|
|
184
|
+
assert.equal(calls.installs.length, 0, 'nothing installed');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test('a move that carries other code too is reported, never applied', () => {
|
|
188
|
+
expectReportedOnly((box) => fs.writeFileSync(path.join(box, 'postinstall.js'), 'evil()'), /other changes besides the core move/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('a move whose package.json changes more than the core pin is reported, never applied', () => {
|
|
192
|
+
expectReportedOnly((box) => {
|
|
193
|
+
const p = JSON.parse(fs.readFileSync(path.join(box, 'package.json'), 'utf8'));
|
|
194
|
+
p.scripts = { prestart: 'evil' };
|
|
195
|
+
fs.writeFileSync(path.join(box, 'package.json'), JSON.stringify(p, null, 2) + '\n');
|
|
196
|
+
}, /package\.json changed beyond the core pin/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('a move onto a tarball from GitHub (not a published release) is reported, never applied', () => {
|
|
200
|
+
// the pin alone names a tarball (the spec gate) …
|
|
201
|
+
expectReportedOnly((box) => writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW), /not a published release/);
|
|
202
|
+
// … and a tarball file brought in with it is caught by the file gate first
|
|
203
|
+
expectReportedOnly((box) => {
|
|
204
|
+
writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW);
|
|
205
|
+
fs.mkdirSync(path.join(box, 'vendor'), { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${NEW}.tgz`), 'unreleased');
|
|
207
|
+
}, /other changes besides the core move/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a lockfile that resolves from outside the public registry is reported, never applied', () => {
|
|
211
|
+
expectReportedOnly((box) => {
|
|
212
|
+
const lock = JSON.parse(fs.readFileSync(path.join(box, 'package-lock.json'), 'utf8'));
|
|
213
|
+
lock.packages['node_modules/left-pad'] = { version: '1.0.0', resolved: 'https://evil.example/left-pad.tgz' };
|
|
214
|
+
fs.writeFileSync(path.join(box, 'package-lock.json'), JSON.stringify(lock, null, 2) + '\n');
|
|
215
|
+
}, /source outside the public registry/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('the core repo itself: a no-op, without a single git spawn', () => {
|
|
219
|
+
const { laptop } = setup();
|
|
220
|
+
const coreRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-core-'));
|
|
221
|
+
fs.writeFileSync(path.join(coreRepo, 'package.json'), JSON.stringify({ name: 'cloud-bongos', version: '0.0.2' }));
|
|
222
|
+
const { run, calls } = stubRun(laptop);
|
|
223
|
+
const logs = [];
|
|
224
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot: coreRepo, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
225
|
+
assert.equal(calls.git, 0);
|
|
226
|
+
assert.deepEqual(logs, []);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('origin unreadable: silent no-op', () => {
|
|
230
|
+
const { laptop, coreRoot, origin } = setup();
|
|
231
|
+
// Drop the remote-tracking ref and point origin at nothing: the fetch fails and there is no origin/main.
|
|
232
|
+
git(laptop, 'update-ref', '-d', 'refs/remotes/origin/main');
|
|
233
|
+
git(laptop, 'remote', 'set-url', 'origin', origin + '-gone');
|
|
234
|
+
const before = head(laptop);
|
|
235
|
+
const { run, calls } = stubRun(laptop);
|
|
236
|
+
const logs = [];
|
|
237
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
238
|
+
assert.deepEqual(logs, [], 'nothing said');
|
|
239
|
+
assert.equal(head(laptop), before);
|
|
240
|
+
assert.equal(calls.installs.length, 0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('no move on origin: silent, and start pays no install', () => {
|
|
244
|
+
const { laptop, coreRoot } = setup({ move: false });
|
|
245
|
+
const { run, calls } = stubRun(laptop);
|
|
246
|
+
const logs = [];
|
|
247
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
248
|
+
assert.deepEqual(logs, []);
|
|
249
|
+
assert.equal(calls.installs.length, 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('a pin the LAPTOP moved (not pushed yet) is not reported as a hub move', () => {
|
|
253
|
+
const { laptop, coreRoot } = setup({ move: false });
|
|
254
|
+
writePin(laptop, '1.21.99', '1.21.99');
|
|
255
|
+
git(laptop, 'commit', '-qam', 'local pin bump');
|
|
256
|
+
const { run } = stubRun(laptop);
|
|
257
|
+
const logs = [];
|
|
258
|
+
const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
|
|
259
|
+
assert.equal(r.action, 'none');
|
|
260
|
+
assert.deepEqual(logs, []);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('a scripts link left dangling after the install is re-pointed at the installed core', () => {
|
|
264
|
+
const { laptop } = setup({ move: false });
|
|
265
|
+
const link = path.join(laptop, 'scripts');
|
|
266
|
+
fs.rmSync(link, { force: true, recursive: false });
|
|
267
|
+
const gone = path.join(laptop, 'old-core-scripts');
|
|
268
|
+
fs.mkdirSync(gone);
|
|
269
|
+
fs.symlinkSync(gone, link, 'junction');
|
|
270
|
+
fs.rmSync(gone, { recursive: true });
|
|
271
|
+
const logs = [];
|
|
272
|
+
assert.equal(pinSync.healScriptsLink(laptop, { log: (s) => logs.push(s) }), 'repointed');
|
|
273
|
+
assert.ok(fs.existsSync(path.join(link, 'gds', 'fitness.js')));
|
|
274
|
+
// a real scripts/ directory (the core repo shape) is never touched
|
|
275
|
+
const real = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-real-'));
|
|
276
|
+
fs.mkdirSync(path.join(real, 'scripts'));
|
|
277
|
+
assert.equal(pinSync.healScriptsLink(real, { log: () => {} }), 'not-a-link');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the dispatcher runs the follow before ship and start', () => {
|
|
281
|
+
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
282
|
+
assert.match(src, /verb === 'ship' \|\| verb === 'start'/);
|
|
283
|
+
assert.match(src, /require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
287
|
+
if (failed) process.exit(1);
|
package/tests/core_pin_sync.mjs
CHANGED
|
@@ -228,9 +228,14 @@ test('ensurePinnedCore end to end: deleting the lockfile or adding an .npmrc on
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
test('the dispatcher runs the check for ship only', () => {
|
|
231
|
+
test('the dispatcher runs the check for ship (and start only after a followed core move)', () => {
|
|
232
|
+
// Since task 1004732 the dispatcher goes through core-origin-follow.ensureCurrentCore,
|
|
233
|
+
// which calls ensurePinnedCore for `ship` always and for `start` only after it
|
|
234
|
+
// fast-forwarded a hub pin move (tests/core_origin_follow.mjs drives both).
|
|
232
235
|
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
233
|
-
assert.match(src, /if \(!COMPILED && verb === 'ship'\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-
|
|
236
|
+
assert.match(src, /if \(!COMPILED && \(verb === 'ship' \|\| verb === 'start'\)\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
|
|
237
|
+
const follow = fs.readFileSync(new URL('../scripts/gds/core-origin-follow.js', import.meta.url), 'utf8');
|
|
238
|
+
assert.match(follow, /if \(verb !== 'ship' && followed\.action !== 'fast-forwarded'\) return null;\s*return pinSync\.ensurePinnedCore/);
|
|
234
239
|
});
|
|
235
240
|
|
|
236
241
|
console.log(`\n${passed} passed, ${failed} failed`);
|