@bongos/core 1.19.737 → 1.19.738
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 -35
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/upgrading-the-core.md +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/go-live.js +18 -44
- package/scripts/gds/init.js +21 -29
- package/scripts/gds/provision-config.js +0 -8
- package/scripts/gds/provision-repo.js +26 -30
- package/scripts/gds/provision.js +3 -5
- package/scripts/gds/start.js +1 -1
- package/scripts/gds/update-channel.js +3 -3
- package/scripts/gds/upgrade.js +17 -29
- package/src/module-api.js +1 -1
- package/tests/go_live.mjs +20 -19
- package/tests/init.mjs +11 -6
- package/tests/provision.mjs +33 -10
- package/tests/start_env_guard.mjs +8 -3
- package/tests/upgrade.mjs +6 -3
- package/scripts/gds/npmrc.js +0 -124
- package/tests/npmrc.mjs +0 -154
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.738",
|
|
6
|
+
"core_contract": "1.19.738",
|
|
7
|
+
"source_commit": "37a35be93e9879af0144594f30a8a894a84502f5",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-14T01:
|
|
9
|
+
"built_at": "2026-09-14T01:52:45.616Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 479,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2107,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2610,
|
|
20
|
+
"tree_sha256": "476bbdb5c1700b8ecf374a05e95a28bc6c61df721e5eb815c65439e08126feab",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2492,7 +2492,7 @@
|
|
|
2492
2492
|
{
|
|
2493
2493
|
"path": "docs/module-api-changelog.md",
|
|
2494
2494
|
"mode": "0000644",
|
|
2495
|
-
"sha256": "
|
|
2495
|
+
"sha256": "63fe92dfdaaacde0ba4dfb7882c97c6b875cd90d9b5f3dac0dacc404dbb1531f"
|
|
2496
2496
|
},
|
|
2497
2497
|
{
|
|
2498
2498
|
"path": "docs/modules-contract.md",
|
|
@@ -2752,7 +2752,7 @@
|
|
|
2752
2752
|
{
|
|
2753
2753
|
"path": "docs/recipes/upgrading-the-core.md",
|
|
2754
2754
|
"mode": "0000644",
|
|
2755
|
-
"sha256": "
|
|
2755
|
+
"sha256": "3691b017562bde760fbe3ff29a5efb80d886dd770f1187dc4673459871cec513"
|
|
2756
2756
|
},
|
|
2757
2757
|
{
|
|
2758
2758
|
"path": "docs/recipes/windows-builders.md",
|
|
@@ -7537,12 +7537,12 @@
|
|
|
7537
7537
|
{
|
|
7538
7538
|
"path": "package-lock.json",
|
|
7539
7539
|
"mode": "0000644",
|
|
7540
|
-
"sha256": "
|
|
7540
|
+
"sha256": "35ace7cf502387465f75cea83b1159bb82349d019555503f51769a264fd19c3e"
|
|
7541
7541
|
},
|
|
7542
7542
|
{
|
|
7543
7543
|
"path": "package.json",
|
|
7544
7544
|
"mode": "0000644",
|
|
7545
|
-
"sha256": "
|
|
7545
|
+
"sha256": "4fbaebd587863651e105d72ed373cef166d16eebd5f86a5d51395d98c33c5f60"
|
|
7546
7546
|
},
|
|
7547
7547
|
{
|
|
7548
7548
|
"path": "public-docs/index.html",
|
|
@@ -8122,7 +8122,7 @@
|
|
|
8122
8122
|
{
|
|
8123
8123
|
"path": "scripts/gds/go-live.js",
|
|
8124
8124
|
"mode": "0000644",
|
|
8125
|
-
"sha256": "
|
|
8125
|
+
"sha256": "f246947d973eec84ea174736bc87bf52b4716c939fc86bff4037b330f6790f9a"
|
|
8126
8126
|
},
|
|
8127
8127
|
{
|
|
8128
8128
|
"path": "scripts/gds/goal-names.js",
|
|
@@ -8157,7 +8157,7 @@
|
|
|
8157
8157
|
{
|
|
8158
8158
|
"path": "scripts/gds/init.js",
|
|
8159
8159
|
"mode": "0000644",
|
|
8160
|
-
"sha256": "
|
|
8160
|
+
"sha256": "3879084ce7246d6fdcf3a053006e86e7d533016b6dbb9b5aa160d65a3e9336f5"
|
|
8161
8161
|
},
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -8299,11 +8299,6 @@
|
|
|
8299
8299
|
"mode": "0000644",
|
|
8300
8300
|
"sha256": "d34cbccec1fa3053f960c1ac0950ce7e6b4b05f9d32a23304c4c3abc41c22ef7"
|
|
8301
8301
|
},
|
|
8302
|
-
{
|
|
8303
|
-
"path": "scripts/gds/npmrc.js",
|
|
8304
|
-
"mode": "0000644",
|
|
8305
|
-
"sha256": "1beffeed111e8528cfa0a629d5ef81c766023de5c6820a81659a390070868228"
|
|
8306
|
-
},
|
|
8307
8302
|
{
|
|
8308
8303
|
"path": "scripts/gds/oauth-secret.js",
|
|
8309
8304
|
"mode": "0000644",
|
|
@@ -8362,7 +8357,7 @@
|
|
|
8362
8357
|
{
|
|
8363
8358
|
"path": "scripts/gds/provision-config.js",
|
|
8364
8359
|
"mode": "0000644",
|
|
8365
|
-
"sha256": "
|
|
8360
|
+
"sha256": "48d13ecda786629148713e0bd89f0d020ee47de107fa5a2327d44989783907ac"
|
|
8366
8361
|
},
|
|
8367
8362
|
{
|
|
8368
8363
|
"path": "scripts/gds/provision-net.js",
|
|
@@ -8377,7 +8372,7 @@
|
|
|
8377
8372
|
{
|
|
8378
8373
|
"path": "scripts/gds/provision-repo.js",
|
|
8379
8374
|
"mode": "0000644",
|
|
8380
|
-
"sha256": "
|
|
8375
|
+
"sha256": "c5822f3b29f90998c831639df2b0925033f17758cc329609de507f5e6e1ad7c8"
|
|
8381
8376
|
},
|
|
8382
8377
|
{
|
|
8383
8378
|
"path": "scripts/gds/provision-units.js",
|
|
@@ -8387,7 +8382,7 @@
|
|
|
8387
8382
|
{
|
|
8388
8383
|
"path": "scripts/gds/provision.js",
|
|
8389
8384
|
"mode": "0000644",
|
|
8390
|
-
"sha256": "
|
|
8385
|
+
"sha256": "614aa63d02f3ae97f39e492cb0948a92b40364e25f4780d87546b64c01e5c69c"
|
|
8391
8386
|
},
|
|
8392
8387
|
{
|
|
8393
8388
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -8857,7 +8852,7 @@
|
|
|
8857
8852
|
{
|
|
8858
8853
|
"path": "scripts/gds/start.js",
|
|
8859
8854
|
"mode": "0000644",
|
|
8860
|
-
"sha256": "
|
|
8855
|
+
"sha256": "7e4b7cd97e9a414cf55e1118c902b4ab17239d3810fe26e5b90aa18d6a948432"
|
|
8861
8856
|
},
|
|
8862
8857
|
{
|
|
8863
8858
|
"path": "scripts/gds/status.js",
|
|
@@ -8902,12 +8897,12 @@
|
|
|
8902
8897
|
{
|
|
8903
8898
|
"path": "scripts/gds/update-channel.js",
|
|
8904
8899
|
"mode": "0000644",
|
|
8905
|
-
"sha256": "
|
|
8900
|
+
"sha256": "1aeeb6171244e1d42ee7189a3aa2ae5d83fd175bfe1ce89300d6ca7511819f0b"
|
|
8906
8901
|
},
|
|
8907
8902
|
{
|
|
8908
8903
|
"path": "scripts/gds/upgrade.js",
|
|
8909
8904
|
"mode": "0000644",
|
|
8910
|
-
"sha256": "
|
|
8905
|
+
"sha256": "7a82fb0cf919e14700ceba718e28da1324df2eab669a847348271c85b2f7bd66"
|
|
8911
8906
|
},
|
|
8912
8907
|
{
|
|
8913
8908
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -9347,7 +9342,7 @@
|
|
|
9347
9342
|
{
|
|
9348
9343
|
"path": "src/module-api.js",
|
|
9349
9344
|
"mode": "0000644",
|
|
9350
|
-
"sha256": "
|
|
9345
|
+
"sha256": "4c5bdcc7f30c8ae47bdb1ecb596b692bd6405a227fc895fa5bfcd33cd8f92340"
|
|
9351
9346
|
},
|
|
9352
9347
|
{
|
|
9353
9348
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10567,7 +10562,7 @@
|
|
|
10567
10562
|
{
|
|
10568
10563
|
"path": "tests/go_live.mjs",
|
|
10569
10564
|
"mode": "0000644",
|
|
10570
|
-
"sha256": "
|
|
10565
|
+
"sha256": "fb937d66f8efa9fdcfb58715785a0c9d94e52034d04175aec78310c70bd53a7f"
|
|
10571
10566
|
},
|
|
10572
10567
|
{
|
|
10573
10568
|
"path": "tests/goal_abuse_matrix.mjs",
|
|
@@ -11287,7 +11282,7 @@
|
|
|
11287
11282
|
{
|
|
11288
11283
|
"path": "tests/init.mjs",
|
|
11289
11284
|
"mode": "0000644",
|
|
11290
|
-
"sha256": "
|
|
11285
|
+
"sha256": "945aaac204e9e60772d3e7b361c97c099d236e6be6b4b794cce36ecb391d8a1a"
|
|
11291
11286
|
},
|
|
11292
11287
|
{
|
|
11293
11288
|
"path": "tests/instance_config.mjs",
|
|
@@ -11679,11 +11674,6 @@
|
|
|
11679
11674
|
"mode": "0000644",
|
|
11680
11675
|
"sha256": "c57c0211709ab041ee895bbb4ade9f4b146b43147570bf282e6d6a808560db76"
|
|
11681
11676
|
},
|
|
11682
|
-
{
|
|
11683
|
-
"path": "tests/npmrc.mjs",
|
|
11684
|
-
"mode": "0000644",
|
|
11685
|
-
"sha256": "1aaa68ee1a4c4a58563cef86aac44cfa0e046ad7376d3a2108bf6dd455580942"
|
|
11686
|
-
},
|
|
11687
11677
|
{
|
|
11688
11678
|
"path": "tests/oauth_secret.mjs",
|
|
11689
11679
|
"mode": "0000644",
|
|
@@ -11982,7 +11972,7 @@
|
|
|
11982
11972
|
{
|
|
11983
11973
|
"path": "tests/provision.mjs",
|
|
11984
11974
|
"mode": "0000644",
|
|
11985
|
-
"sha256": "
|
|
11975
|
+
"sha256": "25fbd2adccf79e921c642f8f57b082f5f19fa01cd981cd5b813dc1070511aadb"
|
|
11986
11976
|
},
|
|
11987
11977
|
{
|
|
11988
11978
|
"path": "tests/provision_restart.mjs",
|
|
@@ -12702,7 +12692,7 @@
|
|
|
12702
12692
|
{
|
|
12703
12693
|
"path": "tests/start_env_guard.mjs",
|
|
12704
12694
|
"mode": "0000644",
|
|
12705
|
-
"sha256": "
|
|
12695
|
+
"sha256": "dd7171dbc59e024de99a063763219237814cf4c943fb3d873cc324f3bfde7f25"
|
|
12706
12696
|
},
|
|
12707
12697
|
{
|
|
12708
12698
|
"path": "tests/start_rebase_warning.mjs",
|
|
@@ -12922,7 +12912,7 @@
|
|
|
12922
12912
|
{
|
|
12923
12913
|
"path": "tests/upgrade.mjs",
|
|
12924
12914
|
"mode": "0000644",
|
|
12925
|
-
"sha256": "
|
|
12915
|
+
"sha256": "def73ab0f532c04df167d77917981eaf7e0dc7e1d2a7e7c85f3f962d7d65ce2a"
|
|
12926
12916
|
},
|
|
12927
12917
|
{
|
|
12928
12918
|
"path": "tests/upgrade_persist_pin.mjs",
|
|
@@ -1948,5 +1948,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1948
1948
|
landed since 1.19.735 with no explicit bump. run 34771773838. (task 1002620)
|
|
1949
1949
|
1.19.737 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1950
1950
|
landed since 1.19.736 with no explicit bump. run 34795026282. (task 1002620)
|
|
1951
|
+
1.19.738 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1952
|
+
landed since 1.19.737 with no explicit bump. run 34797297122. (task 1002620)
|
|
1951
1953
|
---------------------------------------------------------------------------
|
|
1952
1954
|
```
|
|
@@ -108,7 +108,7 @@ node .claude/scheduled-tasks/core-update-subscription/subscribe.js #
|
|
|
108
108
|
node .claude/scheduled-tasks/core-update-subscription/subscribe.js --apply # upgrade
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Each subscribed instance: read its installed core → ask the registry (`npm view`,
|
|
111
|
+
Each subscribed instance: read its installed core → ask the registry (`npm view`, no credential needed — the package is public) → resolve the newest version its channel permits → if newer, `bongos upgrade --to <v> --registry` with **auto-rollback ON**. It prints one line per instance plus a tally; a non-zero exit means an upgrade failed (already auto-reverted).
|
|
112
112
|
|
|
113
113
|
**Triple-gated, so nothing moves by accident:** the `core-update-subscription` routine is `requiresAutonomy: true` + **default OFF** in [`config/scheduled-routines.json`](../../config/scheduled-routines.json), the roster ships empty, and every upgrade is health-gated. Arm it by enabling the routine's timer on the control plane once autonomy is on.
|
|
114
114
|
|
|
@@ -116,7 +116,8 @@ Each subscribed instance: read its installed core → ask the registry (`npm vie
|
|
|
116
116
|
|
|
117
117
|
## Gotchas
|
|
118
118
|
|
|
119
|
-
- **`--registry` no
|
|
119
|
+
- **`--registry` needs no credential at all** (task 1003878). `@bongos/core` is **public** on npm (task 1003872), so `bongos upgrade --registry` resolves it from the default registry like any other dependency — no token, no login, and no instance `.npmrc`. An install failure on this path is therefore a *real* failure (network, registry outage, or a version that was never published), never a missing credential.
|
|
120
|
+
*History, so the symptom is recognisable on an old instance:* the core used to publish private, and the instance `.npmrc` read `_authToken=<redacted> npm gives that per-project key precedence, so with the env var unset it expanded EMPTY and **masked a perfectly valid literal token in `~/.npmrc`** — npm went anonymous, the private core 404'd, and the error blamed a credential the box actually held. Task 1002754 worked around it by resolving the token in-process; task 1003878 deleted the whole mechanism along with the `.npmrc` that caused it. An instance still carrying a committed `.npmrc` from a pre-1003878 scaffold can safely delete it.
|
|
120
121
|
- **A green `publish` run is not evidence of a publish.** The publish-on-merge lane succeeds on its no-op paths too (cascade terminator, armed-gate). Read the run's **job summary**, which now states the verdict explicitly: `PUBLISHED @bongos/core@<v>` vs `NO-OP — nothing was published this run`.
|
|
121
122
|
- **`PGDATABASE` is load-bearing on a co-tenant droplet.** `migrate.sh` defaults `PGDATABASE` to `example` — **production**. Always set `PGDATABASE=<instance-db>` explicitly, or you migrate the wrong database.
|
|
122
123
|
- **The health check polls the restart** (fixed in task [#1984](https://example.com/builders#/task/1984)). The service binds its port ~1s after `systemctl restart`, so `--health-url` now polls up to ~30s for a 2xx before reporting a warning — an immediate `fetch failed` no longer means a failed upgrade. (Pre-1984 cores fetched once and spuriously reported `fetch failed`; there, confirm with `is-active` + a manual `curl`.)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.738",
|
|
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.738",
|
|
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.738",
|
|
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/go-live.js
CHANGED
|
@@ -84,7 +84,7 @@ function isSafeToken(v) {
|
|
|
84
84
|
// unit name that the script would then try to `systemctl stop`. Absent, the stop and
|
|
85
85
|
// the restart are simply skipped: there is no `reset --hard` to race.
|
|
86
86
|
const REQUIRED_FIELDS = ['instanceDir', 'service', 'healthUrl', 'versionUrl'];
|
|
87
|
-
const OPTIONAL_FIELDS = ['ssh', 'local', 'deployTimer', 'deployService', '
|
|
87
|
+
const OPTIONAL_FIELDS = ['ssh', 'local', 'deployTimer', 'deployService', 'runAs', 'backupDb', 'backupDir', 'env', 'registryPackage', 'pinMode'];
|
|
88
88
|
|
|
89
89
|
// How the bump is made durable — the other half of co-tenant mode (task 1003521).
|
|
90
90
|
//
|
|
@@ -153,7 +153,6 @@ const CONFIG_TEMPLATE = `{
|
|
|
153
153
|
"versionUrl": "http://127.0.0.1:3002/version", // box-side truth read back after
|
|
154
154
|
"deployTimer": "my-deploy.timer", // optional: pull-deploy timer (its reset --hard is the hazard)
|
|
155
155
|
"deployService": "my-deploy.service", // optional: forced tick proves the pin stuck
|
|
156
|
-
"npmEnvFile": "/etc/my/npm.env", // optional: root-only NPM_TOKEN for a PRIVATE package
|
|
157
156
|
"runAs": "me", // optional: unprivileged owner of the instance
|
|
158
157
|
"backupDb": "mydb", // optional: pg_dump before the bump
|
|
159
158
|
"backupDir": "~/backups",
|
|
@@ -247,40 +246,19 @@ function remote(target, script, { run = spawnSync, timeoutMs = 900000 } = {}) {
|
|
|
247
246
|
// `sudo -u <user> -H env K=V … <cmd>` — the shape every runbook entry uses. Returns
|
|
248
247
|
// a string safe to embed because every component was allowlist-validated.
|
|
249
248
|
//
|
|
250
|
-
// A privileged child needs npm auth, and there is NO WAY to hand it a secret through
|
|
251
|
-
// sudo without logging it (task 1003218, corrected).
|
|
252
|
-
//
|
|
253
|
-
// `env NPM_TOKEN="$NPM_TOKEN" …` → sudo logs it in COMMAND=
|
|
254
|
-
// `sudo --preserve-env=NPM_TOKEN …` → sudo logs it in ENV=
|
|
255
|
-
//
|
|
256
|
-
// The first was the original bug; the second was my "fix", which only MOVED the value
|
|
257
|
-
// to a different field of the same log line. Both put a live credential in a
|
|
258
|
-
// persistent journal. What matters is that sudo logs its whole invocation, so the
|
|
259
|
-
// secret must never be part of it.
|
|
260
|
-
//
|
|
261
|
-
// So the child DERIVES the token itself, from a file it can already read. Only the
|
|
262
|
-
// `sed` appears in argv; the value never does. This is the shape docs/recipes/
|
|
263
|
-
// core-release-pipeline.md already documents as the safe one. The guard matters as
|
|
264
|
-
// much as the derivation: an EMPTY token silently sends npm to the public registry,
|
|
265
|
-
// which 404s the private package, which auto-rolls-back "cleanly" having taught you
|
|
266
|
-
// nothing — so an empty read aborts loudly instead of half-running.
|
|
267
|
-
//
|
|
268
|
-
// (The nightly lane never takes this path: it runs under systemd as the owner with an
|
|
269
|
-
// EnvironmentFile, no sudo, and leaks nothing. This is the manual/agent path only.)
|
|
270
|
-
const NPM_AUTH_PREAMBLE = [
|
|
271
|
-
'export NPM_TOKEN="${NPM_TOKEN:-$(sed -n \'s/.*_authToken=//p\' "$HOME/.npmrc" 2>/dev/null | head -1)}"',
|
|
272
|
-
'[ -n "$NPM_TOKEN" ] || { echo "go-live: EMPTY NPM_TOKEN — npm would 404 the private core and roll back; aborting" >&2; exit 12; }',
|
|
273
|
-
].join('; ');
|
|
274
|
-
|
|
275
249
|
// `sudo -u <user> -H env K=V … <cmd>` — the shape every runbook entry uses. Returns
|
|
276
250
|
// a string safe to embed because every component was allowlist-validated.
|
|
277
251
|
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
252
|
+
// Only NON-secret configuration (PGDATABASE, HOME) travels in the plain `env K=V` list —
|
|
253
|
+
// seeing those in a log is useful rather than harmful.
|
|
254
|
+
//
|
|
255
|
+
// This used to take a `needsNpm` option wrapping the command in a preamble that DERIVED an
|
|
256
|
+
// npm credential on the far side, because sudo logs its whole invocation and there was no way
|
|
257
|
+
// to hand a privileged child a secret without journalling it (task 1003218). @bongos/core is
|
|
258
|
+
// public (task 1003872), so the deploy path needs no npm credential at all and the whole
|
|
259
|
+
// hazard is gone rather than mitigated (task 1003878).
|
|
260
|
+
function asOwner(target, cmd) {
|
|
261
|
+
const inner = cmd;
|
|
284
262
|
const envPairs = Object.entries(target.env || {}).map(([k, v]) => `${k}=${v}`);
|
|
285
263
|
if (!target.runAs) {
|
|
286
264
|
const joined = envPairs.join(' ');
|
|
@@ -622,23 +600,21 @@ function renderReport(r) {
|
|
|
622
600
|
// Phases
|
|
623
601
|
// ---------------------------------------------------------------------------
|
|
624
602
|
|
|
625
|
-
// Registry proof + optional content proof, in ONE round-trip. The registry is
|
|
626
|
-
// consulted ON THE BOX because
|
|
627
|
-
//
|
|
603
|
+
// Registry proof + optional content proof, in ONE round-trip. The registry is still
|
|
604
|
+
// consulted ON THE BOX — not because auth lives there (the package is public now), but
|
|
605
|
+
// because the box's answer is the one that matters: its network, its resolver, its cache.
|
|
628
606
|
function probeRemote(target, targetVersion, marker, opts = {}) {
|
|
629
607
|
const pkg = target.registryPackage || '@bongos/core';
|
|
630
|
-
const src = target.npmEnvFile ? `set -a; . ${target.npmEnvFile}; set +a` : 'true';
|
|
631
608
|
const script = `
|
|
632
609
|
set -uo pipefail
|
|
633
|
-
${src}
|
|
634
610
|
echo "##REGISTRY##"
|
|
635
|
-
${asOwner(target, `npm view ${pkg}@${targetVersion} version
|
|
611
|
+
${asOwner(target, `npm view ${pkg}@${targetVersion} version`)} 2>&1 | tail -1
|
|
636
612
|
echo "##TREE##"
|
|
637
613
|
( cd ${target.instanceDir} && ${target.runAs ? `sudo -u ${target.runAs} -H ` : ''}git status --porcelain | head -20 )
|
|
638
614
|
echo "##INSTALLED##"
|
|
639
615
|
${asOwner(target, `node -e "console.log(require('${target.instanceDir}/node_modules/${pkg}/package.json').version)"`)} 2>&1 | tail -1
|
|
640
616
|
${marker ? `echo "##MARKER##"
|
|
641
|
-
${asOwner(target, `sh -c ${shq(markerProbe(pkg, targetVersion, marker))}
|
|
617
|
+
${asOwner(target, `sh -c ${shq(markerProbe(pkg, targetVersion, marker))}`)}` : ''}
|
|
642
618
|
echo "##END##"
|
|
643
619
|
`;
|
|
644
620
|
const res = remote(target, script, opts);
|
|
@@ -709,7 +685,6 @@ function applyGoLive(target, targetName, targetVersion, from, opts = {}) {
|
|
|
709
685
|
const pkg = target.registryPackage || '@bongos/core';
|
|
710
686
|
const pinMode = pinModeOf(target);
|
|
711
687
|
const steps = [];
|
|
712
|
-
const src = target.npmEnvFile ? `set -a; . ${target.npmEnvFile}; set +a` : 'true';
|
|
713
688
|
|
|
714
689
|
// Phase A — stop the pull-deploy timer, then back up. The timer's `reset --hard`
|
|
715
690
|
// is the hazard: it would revert an uncommitted pin mid-flight. A co-tenant has no
|
|
@@ -738,8 +713,7 @@ echo "##END##"
|
|
|
738
713
|
const upgrade = upgradeCommand(target, targetVersion);
|
|
739
714
|
const phaseB = remote(target, `
|
|
740
715
|
set -uo pipefail
|
|
741
|
-
${
|
|
742
|
-
${asOwner(target, `bash -c "cd ${target.instanceDir} && ${upgrade}"`, { needsNpm: true })}
|
|
716
|
+
${asOwner(target, `bash -c "cd ${target.instanceDir} && ${upgrade}"`)}
|
|
743
717
|
`, opts);
|
|
744
718
|
const rolledBack = /rolled back/i.test(phaseB.stdout + phaseB.stderr);
|
|
745
719
|
steps.push({
|
|
@@ -995,7 +969,7 @@ if (require.main === module) {
|
|
|
995
969
|
module.exports = {
|
|
996
970
|
loadConfig, selectTarget, validateTarget, planGoLive, artistGateEntries, readArtistGate, ARTIST_GATE_PATH, ARTIST_GATE_LEVELS,
|
|
997
971
|
parseMarker, parseMarkerHits,
|
|
998
|
-
renderPlan, renderReport, failureExcerpt, asOwner, isSafeToken,
|
|
972
|
+
renderPlan, renderReport, failureExcerpt, asOwner, isSafeToken, refreshMain, isLocalTarget, gitRefExists, SUDO_PREAMBLE,
|
|
999
973
|
applyGoLive, upgradeCommand, pinStep, pinModeOf,
|
|
1000
974
|
CONFIG_TEMPLATE, HALTS, SEMVER_RE, PIN_MODES, DEFAULT_PIN_MODE,
|
|
1001
975
|
};
|
package/scripts/gds/init.js
CHANGED
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
// bongos init --vendor-core <tgz> greenfield only: vendor the @bongos/core .tgz + pin
|
|
60
60
|
// package.json to file:vendor/… + generate package-lock.json,
|
|
61
61
|
// so the scaffold is `npm ci`-installable (ADR 0108 gap 1/4).
|
|
62
|
-
// Without it the scaffold pins the caret range
|
|
63
|
-
// resolves from the
|
|
64
|
-
// bongos init --registry-core greenfield only: keep the caret pin
|
|
65
|
-
// .
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
62
|
+
// Without it the scaffold pins the caret range, which
|
|
63
|
+
// resolves from the public npm registry (--registry-core).
|
|
64
|
+
// bongos init --registry-core greenfield only: keep the caret pin and generate
|
|
65
|
+
// package-lock.json from the registry. @bongos/core is
|
|
66
|
+
// PUBLIC (task 1003872), so this needs no token, no login
|
|
67
|
+
// and no .npmrc — it resolves from the default registry
|
|
68
|
+
// like any other dependency.
|
|
69
69
|
// Mutually exclusive with --vendor-core (registry vs tarball).
|
|
70
70
|
|
|
71
71
|
const fs = require('node:fs');
|
|
@@ -78,7 +78,6 @@ const { materializeClaude } = require('./claude-materialize'); // ADR 0108 §3 (
|
|
|
78
78
|
const { detectStack, formatStackSummary } = require('./adopt-detect'); // ADR 0121 §Decision 4 (task 2043): inspect an existing repo before layering
|
|
79
79
|
const { resolveBacklogTasks, DEFAULT_MAX: BACKLOG_MAX } = require('./adopt-import'); // ADR 0121 §Decision 3 (task 2044): import an existing backlog
|
|
80
80
|
const { preflightAdopt, formatPreflight } = require('./adopt-preflight'); // ADR 0121 §Decision 5 (task 2045): the accept-gated conflict pre-flight
|
|
81
|
-
const { writeNpmrc, hasNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the registry-install path's env-fed .npmrc
|
|
82
81
|
|
|
83
82
|
const KNOWN_MODULES = ['game', 'art-pipeline', 'discord', 'dev-box'];
|
|
84
83
|
|
|
@@ -946,7 +945,7 @@ async function main(argv) {
|
|
|
946
945
|
const adopt = hasFlag('--adopt', args); // brownfield: LAYER onto an existing repo (ADR 0121)
|
|
947
946
|
const accept = hasFlag('--accept', args) || hasFlag('--yes', args) || hasFlag('-y', args); // adopt pre-flight accept-gate (§Decision 5)
|
|
948
947
|
const vendorCore = arg('--vendor-core', args); // greenfield: vendor + file:-pin a core .tgz, then generate the lockfile (ADR 0108 gap 1/4, task 2053)
|
|
949
|
-
const registryCore = hasFlag('--registry-core', args); // greenfield: caret pin
|
|
948
|
+
const registryCore = hasFlag('--registry-core', args); // greenfield: keep the caret pin and generate the lockfile from the public registry (task 1003878)
|
|
950
949
|
|
|
951
950
|
if (adopt && force) {
|
|
952
951
|
console.error('init: --force is a greenfield-only overwrite; --adopt never overwrites, so --force is ignored.');
|
|
@@ -1030,30 +1029,23 @@ async function main(argv) {
|
|
|
1030
1029
|
}
|
|
1031
1030
|
}
|
|
1032
1031
|
|
|
1033
|
-
// Registry-install path
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1032
|
+
// Registry-install path: keep the caret pin writeConfigs wrote and generate
|
|
1033
|
+
// package-lock.json from the registry, so a later `npm ci` is reproducible.
|
|
1034
|
+
//
|
|
1035
|
+
// This used to write an env-fed npm auth file and skip the lockfile when no credential was
|
|
1036
|
+
// present, because @bongos/core published PRIVATE. It is public now (task 1003872), so it
|
|
1037
|
+
// resolves from the default registry like any other dependency: no auth file, no credential,
|
|
1038
|
+
// and no tokenless-404 branch to explain (task 1003878).
|
|
1039
1039
|
if (registryCore) {
|
|
1040
1040
|
if (dryRun) {
|
|
1041
|
-
console.log(
|
|
1042
|
-
? 'run `npm install` to generate package-lock.json from the registry'
|
|
1043
|
-
: 'SKIP lockfile generation (no NPM_TOKEN — the private core 404s until one is provided)'}`);
|
|
1041
|
+
console.log('\n[dry-run] would run `npm install` to generate package-lock.json from the registry');
|
|
1044
1042
|
} else {
|
|
1045
|
-
console.log('\nWiring the registry-install path
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1049
|
-
if (!inst.ok) {
|
|
1050
|
-
console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — confirm NPM_TOKEN reaches the private @bongos org; package-lock.json not generated.`);
|
|
1051
|
-
} else {
|
|
1052
|
-
console.log(' ✓ npm install → package-lock.json generated (pulled @bongos/core from the registry)');
|
|
1053
|
-
}
|
|
1043
|
+
console.log('\nWiring the registry-install path:');
|
|
1044
|
+
const inst = upgrade.npmInstall(dir);
|
|
1045
|
+
if (!inst.ok) {
|
|
1046
|
+
console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated. @bongos/core is public, so this is a real failure (network, registry outage, or an unpublished version), not a missing credential.`);
|
|
1054
1047
|
} else {
|
|
1055
|
-
console.log('
|
|
1056
|
-
console.log(' Provide a read-only automation token (paid `bongos` org) as NPM_TOKEN before `npm ci`; without it the private package 404s cleanly.');
|
|
1048
|
+
console.log(' ✓ npm install → package-lock.json generated (pulled @bongos/core from the registry)');
|
|
1057
1049
|
}
|
|
1058
1050
|
}
|
|
1059
1051
|
}
|
|
@@ -19,12 +19,6 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
19
19
|
// plain require (its require.main guard means importing it runs no CLI).
|
|
20
20
|
const oauthSecret = require('./oauth-secret');
|
|
21
21
|
|
|
22
|
-
// The registry-install gates + env-fed .npmrc (ADR 0134, task 2090). Lets the
|
|
23
|
-
// standalone scaffold OPT IN to pulling the PRIVATE @bongos/core from the npm
|
|
24
|
-
// registry (PROVISION_CORE_FROM_REGISTRY=1) instead of vendoring a tarball, with a
|
|
25
|
-
// fail-soft fallback to the vendored path when no NPM_TOKEN is present.
|
|
26
|
-
const { registryCoreRequested, hasNpmToken } = require('./npmrc');
|
|
27
|
-
|
|
28
22
|
// The HTTP User-Agent the manifest conversion stamps its GitHub call with (GitHub
|
|
29
23
|
// requires a UA). Overridable so a fork can identify itself; not secret.
|
|
30
24
|
const MANIFEST_UA = process.env.PROVISION_GITHUB_UA || 'cloudbongos-provisioner';
|
|
@@ -195,10 +189,8 @@ module.exports = {
|
|
|
195
189
|
REPO_ROOT,
|
|
196
190
|
coreVersionSafe,
|
|
197
191
|
hasFlag,
|
|
198
|
-
hasNpmToken,
|
|
199
192
|
loadDeps,
|
|
200
193
|
numEnv,
|
|
201
194
|
oauthSecret,
|
|
202
195
|
provisionerBotEmail,
|
|
203
|
-
registryCoreRequested,
|
|
204
196
|
};
|
|
@@ -14,7 +14,7 @@ const crypto = require('node:crypto');
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
-
const { CONFIG, REPO_ROOT, coreVersionSafe,
|
|
17
|
+
const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail } = require('./provision-config.js');
|
|
18
18
|
// The repo-identity + owner-credential leaf (task 1003081). Carved OUT of this file so
|
|
19
19
|
// upgrade.js can reach these three without pulling provision*.js into the merge
|
|
20
20
|
// resolver's executed closure; re-exported below so every existing caller is unchanged.
|
|
@@ -602,18 +602,20 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
602
602
|
exec(`git -C ${root} checkout -B main`);
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
-
// Core-install path
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
605
|
+
// Core-install path: vendor a tarball built from THIS control-plane checkout
|
|
606
|
+
// (offline, proven — task 2053).
|
|
607
|
+
//
|
|
608
|
+
// This used to be a fork: an opt-in env var selected the registry, and a missing npm
|
|
609
|
+
// credential silently failed it soft back to vendored. Both halves were auth-era and both
|
|
610
|
+
// are gone (task 1003878) — @bongos/core is public, so nothing needs a credential, and an
|
|
611
|
+
// env var that silently changed which install path ran is the divergence behind task
|
|
612
|
+
// 1003520. tests/provision.mjs pins both retired names and proves they are inert.
|
|
613
|
+
//
|
|
614
|
+
// Vendored stays the default here for a reason that has nothing to do with auth and
|
|
615
|
+
// survives the package going public: THIS CHECKOUT IS ROUTINELY AHEAD OF THE REGISTRY.
|
|
616
|
+
// A control plane on an unreleased CORE_VERSION has no published version to install, so
|
|
617
|
+
// the registry path would 404 on exactly the commits an operator most wants to stand up.
|
|
618
|
+
// Whether the vendored path still earns its keep at all is gate 6's call (task 1003879).
|
|
617
619
|
|
|
618
620
|
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
619
621
|
// --no-seed: the instance's own DB is seeded by migrate, not here (and on the adopt path
|
|
@@ -627,27 +629,21 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
627
629
|
// BLOCKING pre-flight finding still hard-stops — it cannot be waved through). init refuses
|
|
628
630
|
// --adopt together with --force, so the two flag sets are mutually exclusive by design.
|
|
629
631
|
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
632
|
+
// Core resolution is vendored (--vendor-core): a tarball built from this checkout,
|
|
633
|
+
// file:-pinned. init still accepts --registry-core for an operator who explicitly wants
|
|
634
|
+
// a published version, and that flag no longer needs any credential.
|
|
634
635
|
const modeFlags = mode === 'adopt' ? '--adopt --accept' : '--force';
|
|
635
636
|
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
636
637
|
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
637
638
|
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
});
|
|
647
|
-
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
648
|
-
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
649
|
-
if (packDest) exec(`rm -rf ${packDest}`);
|
|
650
|
-
}
|
|
639
|
+
// Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
|
|
640
|
+
// with the re-provision pin refresh, task 1003050).
|
|
641
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
642
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
643
|
+
});
|
|
644
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
645
|
+
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
646
|
+
if (packDest) exec(`rm -rf ${packDest}`);
|
|
651
647
|
|
|
652
648
|
// 4. commit + push to the instance's own main (keep node_modules out).
|
|
653
649
|
//
|
package/scripts/gds/provision.js
CHANGED
|
@@ -360,11 +360,9 @@ async function provisionInstance(inst, deps) {
|
|
|
360
360
|
// repo + install the pinned @bongos/core, so the migrate + service below run
|
|
361
361
|
// from the INSTANCE root, not this control-plane checkout. cwd = standaloneRoot.
|
|
362
362
|
// boxExec = the control plane (where scaffoldStandaloneRepo just created the dir).
|
|
363
|
-
//
|
|
364
|
-
// .
|
|
365
|
-
//
|
|
366
|
-
// scaffold already gated on the token, so it is present; the VENDORED default needs
|
|
367
|
-
// no token (the tarball is committed). A tokenless registry `npm ci` 404s cleanly.
|
|
363
|
+
// CORE RESOLUTION: the vendored default needs no network at all (the tarball is
|
|
364
|
+
// committed). A registry pin resolves from the public registry — @bongos/core is public
|
|
365
|
+
// (task 1003872), so `npm ci` needs no credential either way (task 1003878).
|
|
368
366
|
const root = standaloneRoot(inst);
|
|
369
367
|
log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
|
|
370
368
|
boxExec(standalonePullCmd(), { cwd: root });
|
package/scripts/gds/start.js
CHANGED
|
@@ -587,7 +587,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
|
587
587
|
out.push(`${surface.strong(`Don't run ${surface.code('bongos dev')} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
|
|
588
588
|
if (depsMissing) {
|
|
589
589
|
out.push('');
|
|
590
|
-
out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is
|
|
590
|
+
out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is ${surface.strong('public')} on npm, so ${surface.code('npm install @bongos/core')} needs no token or login; a failure here is a real error, not the expected tokenless 404 it used to be.)`);
|
|
591
591
|
}
|
|
592
592
|
return out.join('\n');
|
|
593
593
|
}
|
|
@@ -122,9 +122,9 @@ function resolveChannelTarget({ installed, channel, available } = {}) {
|
|
|
122
122
|
|
|
123
123
|
// ---- registry: what versions of the core are published? ---------------------
|
|
124
124
|
|
|
125
|
-
// List published versions of @bongos/core from the registry via `npm view … versions --json
|
|
126
|
-
//
|
|
127
|
-
//
|
|
125
|
+
// List published versions of @bongos/core from the registry via `npm view … versions --json`.
|
|
126
|
+
// The package is public (task 1003872), so this needs no credential and no instance-local
|
|
127
|
+
// `.npmrc` — any cwd resolves it. Fail-SOFT: any error returns
|
|
128
128
|
// { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
|
|
129
129
|
// `pkg` are injectable for tests.
|
|
130
130
|
function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -32,7 +32,6 @@ const path = require('node:path');
|
|
|
32
32
|
const crypto = require('node:crypto');
|
|
33
33
|
const { spawnSync } = require('node:child_process');
|
|
34
34
|
const { arg, hasFlag } = require('./cli-lib');
|
|
35
|
-
const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc; resolveNpmToken task 1002754
|
|
36
35
|
|
|
37
36
|
const CORE_PKG = '@bongos/core';
|
|
38
37
|
const ARTIFACT = 'bongos-core';
|
|
@@ -98,9 +97,9 @@ function writePin(instanceDir, vendorRel, fsImpl = fs) {
|
|
|
98
97
|
return prev;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
// Rewrite the consumer package.json to pin an EXACT registry version
|
|
102
|
-
//
|
|
103
|
-
//
|
|
100
|
+
// Rewrite the consumer package.json to pin an EXACT registry version: the registry sibling
|
|
101
|
+
// of writePin. `npm install` then resolves @bongos/core@version from the public registry —
|
|
102
|
+
// no credential, since the package is public (task 1003872). Returns the previous dep string.
|
|
104
103
|
function writeRegistryPin(instanceDir, version, fsImpl = fs) {
|
|
105
104
|
const pkgPath = path.join(instanceDir, 'package.json');
|
|
106
105
|
const pkg = readJson(pkgPath, fsImpl);
|
|
@@ -478,13 +477,13 @@ function reportModulePreflight(mods, targetVersion, { log = console.log, err = c
|
|
|
478
477
|
return lines;
|
|
479
478
|
}
|
|
480
479
|
|
|
481
|
-
function npmInstall(instanceDir, run = spawnSync
|
|
480
|
+
function npmInstall(instanceDir, run = spawnSync) {
|
|
482
481
|
// `npm install` (NOT `npm ci`): the pin just changed, so the lockfile must be regenerated from the
|
|
483
482
|
// new tarball; `npm ci` would refuse on the package.json↔lock divergence.
|
|
484
|
-
//
|
|
485
|
-
//
|
|
483
|
+
// This used to take an extraEnv carrying a resolved npm credential, because the instance
|
|
484
|
+
// `.npmrc`'s env expansion could shadow a valid user-level one (task 1002754). The core is
|
|
485
|
+
// public and writes no `.npmrc`, so there is no credential to resolve (task 1003878).
|
|
486
486
|
const opts = { cwd: instanceDir, stdio: 'inherit' };
|
|
487
|
-
if (extraEnv) opts.env = { ...process.env, ...extraEnv };
|
|
488
487
|
const r = run('npm', ['install', '--no-audit', '--no-fund'], opts);
|
|
489
488
|
return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
|
|
490
489
|
}
|
|
@@ -983,11 +982,8 @@ async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts },
|
|
|
983
982
|
try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
|
|
984
983
|
catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
|
|
985
984
|
|
|
986
|
-
// 2. reinstall the previous core.
|
|
987
|
-
|
|
988
|
-
// must never fail on the env-shadowing trap the forward install just avoided.
|
|
989
|
-
const rbCred = resolveNpmToken();
|
|
990
|
-
const inst = npmInstall(instanceDir, run, rbCred.source === 'user-npmrc' ? { NPM_TOKEN: rbCred.token } : null);
|
|
985
|
+
// 2. reinstall the previous core.
|
|
986
|
+
const inst = npmInstall(instanceDir, run);
|
|
991
987
|
result.reinstallOk = !!inst.ok;
|
|
992
988
|
if (inst.ok) log(' ✓ reinstalled previous core');
|
|
993
989
|
else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
|
|
@@ -1094,24 +1090,16 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1094
1090
|
return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
|
|
1095
1091
|
}
|
|
1096
1092
|
|
|
1097
|
-
// 2. bump the pin. Two mutually exclusive core sources
|
|
1098
|
-
// REGISTRY (--registry): pin @bongos/core@<version>
|
|
1099
|
-
// the
|
|
1093
|
+
// 2. bump the pin. Two mutually exclusive core sources:
|
|
1094
|
+
// REGISTRY (--registry): pin @bongos/core@<version> and let `npm install` pull it from
|
|
1095
|
+
// the public registry. No credential — the package is public (task 1003872), which
|
|
1096
|
+
// also retired the whole resolve-a-token-and-inject-it dance (task 1002754/1003878).
|
|
1100
1097
|
// VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
|
|
1101
|
-
let installEnv = null;
|
|
1102
1098
|
if (opts.registry) {
|
|
1103
1099
|
if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
|
|
1104
|
-
// task 1002754 — resolve the credential HERE instead of demanding a hand-exported
|
|
1105
|
-
// env var: env NPM_TOKEN wins, else the user-level ~/.npmrc's literal token is
|
|
1106
|
-
// injected into the install's env so the instance `.npmrc`'s `${NPM_TOKEN}`
|
|
1107
|
-
// expansion (which OUTRANKS the user-level key and expands EMPTY when unset)
|
|
1108
|
-
// can never shadow a working credential again.
|
|
1109
|
-
const cred = resolveNpmToken();
|
|
1110
|
-
if (!cred.token) log(' ! no NPM_TOKEN in env and no literal token in ~/.npmrc — `npm install` will 404 on the PRIVATE @bongos/core until a read-only automation token is provided (ADR 0134).');
|
|
1111
|
-
else if (cred.source === 'user-npmrc') { installEnv = { NPM_TOKEN: cred.token }; log(' ✓ registry credential: inherited from the user-level ~/.npmrc (no NPM_TOKEN export needed — task 1002754)'); }
|
|
1112
1100
|
const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
|
|
1113
|
-
if (!dryRun) {
|
|
1114
|
-
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${
|
|
1101
|
+
if (!dryRun) { snapshot = { prevPin, prevVersion: fromVersion }; }
|
|
1102
|
+
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
1115
1103
|
} else {
|
|
1116
1104
|
let vendorRel = opts.pinPath || (opts.from ? null : vendorRelFor(targetVersion));
|
|
1117
1105
|
if (opts.from) {
|
|
@@ -1130,7 +1118,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1130
1118
|
if (dryRun) { log(' [dry-run] would: npm install → npm run migrate → materialize .claude → regen API docs+client → restart → verify'); return { ok: true, dryRun: true, fromVersion, toVersion: targetVersion }; }
|
|
1131
1119
|
|
|
1132
1120
|
// 3. npm install.
|
|
1133
|
-
const inst = npmInstall(instanceDir, run
|
|
1121
|
+
const inst = npmInstall(instanceDir, run);
|
|
1134
1122
|
if (!inst.ok) {
|
|
1135
1123
|
const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
|
|
1136
1124
|
if (rollbackOnFailure && snapshot) {
|
|
@@ -1370,7 +1358,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1370
1358
|
'',
|
|
1371
1359
|
' --to <version> target core version — presence switches from pre-check to a real bump',
|
|
1372
1360
|
' --from <tgz> source tarball to vendor into vendor/ (else it must already be there)',
|
|
1373
|
-
' --registry pull @bongos/core@<--to> from the npm registry (
|
|
1361
|
+
' --registry pull @bongos/core@<--to> from the public npm registry (no token needed) instead of vendoring a tarball; mutually exclusive with --from',
|
|
1374
1362
|
' --instance <dir> consumer instance root (default: cwd)',
|
|
1375
1363
|
' --service <unit> systemd unit to restart after applying',
|
|
1376
1364
|
' --restart-cmd <cmd> explicit restart command (overrides --service)',
|
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.738'; // 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/go_live.mjs
CHANGED
|
@@ -442,26 +442,27 @@ test("'could not check' is a distinct halt from 'absent'", () => {
|
|
|
442
442
|
// `env K=V` list that omits it drops it too. This cost a real failed go-live.
|
|
443
443
|
// ---------------------------------------------------------------------------
|
|
444
444
|
|
|
445
|
-
test('a privileged npm command
|
|
446
|
-
const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install'
|
|
447
|
-
//
|
|
448
|
-
// sudo's COMMAND= field
|
|
449
|
-
// a different field of the SAME log line, which
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
assert.
|
|
456
|
-
assert.
|
|
445
|
+
test('a privileged npm command carries NO credential machinery at all', () => {
|
|
446
|
+
const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install');
|
|
447
|
+
// HISTORY, so nobody reintroduces it (task 1003218, twice). `env NPM_TOKEN="$NPM_TOKEN"`
|
|
448
|
+
// put the value in sudo's COMMAND= field; `--preserve-env=NPM_TOKEN` moved it to sudo's
|
|
449
|
+
// ENV= field — a different field of the SAME log line, which was never a fix. The third
|
|
450
|
+
// shape had the child DERIVE the token from a file so only a `sed` appeared in argv.
|
|
451
|
+
//
|
|
452
|
+
// All three are now moot rather than mitigated: @bongos/core is public (task 1003872), so
|
|
453
|
+
// the deploy path needs no npm credential and the hazard cannot recur (task 1003878). This
|
|
454
|
+
// asserts the ABSENCE, which is a stronger guarantee than any of the three mitigations.
|
|
455
|
+
assert.doesNotMatch(cmd, /NPM_TOKEN/, 'no token by value, by reference, or by derivation');
|
|
456
|
+
assert.doesNotMatch(cmd, /--preserve-env/, '--preserve-env would log the value in ENV=');
|
|
457
|
+
assert.doesNotMatch(cmd, /_authToken/, 'nothing reads a credential out of an .npmrc');
|
|
457
458
|
// Non-secret config still travels plainly.
|
|
458
459
|
assert.match(cmd, /PGDATABASE=cb/);
|
|
459
460
|
});
|
|
460
461
|
|
|
461
|
-
test('
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
assert.
|
|
462
|
+
test('the retired auth preamble is really gone from the module surface', () => {
|
|
463
|
+
// It was exported and embedded into every privileged command. A re-export would mean the
|
|
464
|
+
// mitigation came back, which now means a credential came back with it.
|
|
465
|
+
assert.equal(G.NPM_AUTH_PREAMBLE, undefined, 'NPM_AUTH_PREAMBLE must not be exported');
|
|
465
466
|
});
|
|
466
467
|
|
|
467
468
|
test('a command that does not need npm is left alone', () => {
|
|
@@ -470,10 +471,10 @@ test('a command that does not need npm is left alone', () => {
|
|
|
470
471
|
assert.ok(!cmd.includes('NPM_TOKEN'));
|
|
471
472
|
});
|
|
472
473
|
|
|
473
|
-
test('with no privilege drop the command still
|
|
474
|
-
const cmd = G.asOwner({ env: { A: 'b' } }, 'npm x'
|
|
474
|
+
test('with no privilege drop the command is still credential-free', () => {
|
|
475
|
+
const cmd = G.asOwner({ env: { A: 'b' } }, 'npm x');
|
|
475
476
|
assert.ok(!cmd.startsWith('sudo'));
|
|
476
|
-
assert.
|
|
477
|
+
assert.doesNotMatch(cmd, /NPM_TOKEN|_authToken/);
|
|
477
478
|
});
|
|
478
479
|
|
|
479
480
|
test('the report surfaces the failing tool’s own words', () => {
|
package/tests/init.mjs
CHANGED
|
@@ -768,10 +768,10 @@ async function runSeedTests() {
|
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
-
// ---- greenfield registry-install path
|
|
771
|
+
// ---- greenfield registry-install path ---------------------------------------
|
|
772
772
|
// main() with --registry-core --dry-run only LOGS (no fs, no network), so we capture
|
|
773
|
-
// console output and assert the plan. The
|
|
774
|
-
//
|
|
773
|
+
// console output and assert the plan. The package is public (task 1003872), so this path
|
|
774
|
+
// writes no `.npmrc` and needs no token; tests/npmrc.mjs went with the module (task 1003878).
|
|
775
775
|
async function runRegistryCoreTests() {
|
|
776
776
|
const at = async (name, fn) => {
|
|
777
777
|
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
@@ -799,14 +799,19 @@ async function runRegistryCoreTests() {
|
|
|
799
799
|
return { code, out: lines.join('\n') };
|
|
800
800
|
};
|
|
801
801
|
|
|
802
|
-
await at('main --registry-core --dry-run: plans
|
|
802
|
+
await at('main --registry-core --dry-run: plans a plain lockfile generation, no .npmrc, no token', async () => {
|
|
803
803
|
const { dir, p } = specFile();
|
|
804
804
|
try {
|
|
805
805
|
const { code, out } = await capture(['--from', p, '--dir', join(dir, 'out'), '--no-seed', '--registry-core', '--dry-run']);
|
|
806
806
|
assert.equal(code, 0);
|
|
807
|
-
assert.match(out, /would
|
|
807
|
+
assert.match(out, /would run `npm install` to generate package-lock\.json/);
|
|
808
808
|
assert.ok(!/vendor/i.test(out), 'no vendored-tarball step on the registry path');
|
|
809
|
-
|
|
809
|
+
// The load-bearing half (task 1003878): a public package resolves from the default
|
|
810
|
+
// registry, so planning an `.npmrc` or mentioning a token would mean the retired
|
|
811
|
+
// auth path came back.
|
|
812
|
+
assert.doesNotMatch(out, /\.npmrc/, 'must not plan an .npmrc');
|
|
813
|
+
assert.doesNotMatch(out, /NPM_TOKEN/, 'must not mention a token');
|
|
814
|
+
assert.equal(existsSync(join(dir, 'out', '.npmrc')), false, 'writes no .npmrc at all');
|
|
810
815
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
811
816
|
});
|
|
812
817
|
|
package/tests/provision.mjs
CHANGED
|
@@ -599,18 +599,41 @@ async function scaffoldWithEnv(env) {
|
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
|
|
602
|
+
// Provisioning used to FORK on two env vars: PROVISION_CORE_FROM_REGISTRY chose the registry
|
|
603
|
+
// path, and a missing NPM_TOKEN silently failed it soft back to the vendored tarball. Both are
|
|
604
|
+
// gone (task 1003878) — the package is public, so nothing needs a token, and an env var that
|
|
605
|
+
// silently changed which install path ran is the divergence behind task 1003520.
|
|
606
|
+
//
|
|
607
|
+
// These two assert the ABSENCE of that fork, which is why they set the retired vars explicitly:
|
|
608
|
+
// a test that merely stopped mentioning them would pass whether or not the fork came back.
|
|
609
|
+
await ta('scaffoldStandaloneRepo: the retired PROVISION_CORE_FROM_REGISTRY env no longer diverts to the registry', async () => {
|
|
603
610
|
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
|
|
604
|
-
assert.match(joined, /
|
|
605
|
-
assert.ok(!/package-core\.js/.test(joined), 'does NOT build a core tarball on the registry path');
|
|
606
|
-
assert.ok(!/--vendor-core/.test(joined), 'does NOT vendor a tarball on the registry path');
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
await ta('scaffoldStandaloneRepo: registry requested but NO NPM_TOKEN → FAILS SOFT to the vendored tarball', async () => {
|
|
610
|
-
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
|
|
611
|
-
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball (vendored fallback)');
|
|
611
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'still vendors — the env var is inert');
|
|
612
612
|
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'file:-pins the vendored tarball');
|
|
613
|
-
assert.ok(!/--registry-core/.test(joined), '
|
|
613
|
+
assert.ok(!/--registry-core/.test(joined), 'the retired env var must not resurrect the registry path');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
await ta('scaffoldStandaloneRepo: a token in env changes NOTHING about the install path', async () => {
|
|
617
|
+
// BOTH runs set the retired request flag and vary ONLY the token — that is precisely the
|
|
618
|
+
// old divergence (flag + token → registry; flag, no token → vendored fail-soft). Comparing
|
|
619
|
+
// two runs that DON'T set the flag would pass whether or not the fork existed, which an
|
|
620
|
+
// adversarial re-add of the fork proved while this test was written that way.
|
|
621
|
+
const withToken = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
|
|
622
|
+
const without = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
|
|
623
|
+
// THE LOAD-BEARING ASSERTION. The old fail-soft made a tokenless standup take a DIFFERENT
|
|
624
|
+
// install path than a tokenized one — two modes diverging on an env var (task 1003520).
|
|
625
|
+
// One public path means the presence of a credential cannot change the outcome at all.
|
|
626
|
+
// Normalize the per-run random temp names (scratch dir, spec file, pack dir) — they differ
|
|
627
|
+
// between any two runs and say nothing about which install path was taken.
|
|
628
|
+
const norm = (t) => t
|
|
629
|
+
.replace(/scaffold-[A-Za-z0-9]+/g, 'SCRATCH')
|
|
630
|
+
.replace(/bongos-init-[^\s.]+/g, 'SPEC')
|
|
631
|
+
.replace(/core-pack-[^\s/]+/g, 'PACK');
|
|
632
|
+
assert.equal(
|
|
633
|
+
norm(withToken),
|
|
634
|
+
norm(without),
|
|
635
|
+
'the command sequence must be identical with and without a token',
|
|
636
|
+
);
|
|
614
637
|
});
|
|
615
638
|
|
|
616
639
|
await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendored tarball path', async () => {
|
|
@@ -107,10 +107,15 @@ t('a hosted origin leads with the board being a website that needs no install',
|
|
|
107
107
|
assert.ok(/healthz/.test(card), 'offers the health probe instead of a bring-up');
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
t('a hosted origin
|
|
110
|
+
t('a hosted origin says the core installs with no token, and never re-offers the retired NPM_TOKEN advice', () => {
|
|
111
111
|
const card = instanceNotRunningCard({ origin: 'https://example-instance.cloudbongos.com', depsMissing: true });
|
|
112
|
-
assert.ok(/
|
|
113
|
-
assert.ok(/
|
|
112
|
+
assert.ok(/public/i.test(card), 'names the package as public — it is (task 1003872)');
|
|
113
|
+
assert.ok(/no token|needs no token/i.test(card), 'tells the reader nothing needs authenticating');
|
|
114
|
+
// The load-bearing half: this card spent months telling people to go find an NPM_TOKEN for
|
|
115
|
+
// a package that is now public. Advising that again would send a newcomer hunting a
|
|
116
|
+
// credential that does not exist and cannot be issued to them.
|
|
117
|
+
assert.doesNotMatch(card, /NPM_TOKEN/, 'must not resurrect the token advice');
|
|
118
|
+
assert.doesNotMatch(card, /\bprivate\b/i, 'must not call the package private');
|
|
114
119
|
});
|
|
115
120
|
|
|
116
121
|
t('isLocalOrigin: loopback is local, everything else is remote', () => {
|
package/tests/upgrade.mjs
CHANGED
|
@@ -444,7 +444,7 @@ t('runUpgrade --registry + --from: refuses (mutually exclusive core sources)', a
|
|
|
444
444
|
assert.match(res.error, /mutually exclusive/);
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
-
t('runUpgrade --registry (full): pins the exact version
|
|
447
|
+
t('runUpgrade --registry (full): pins the exact version, writes NO .npmrc, then install→migrate→verify', async () => {
|
|
448
448
|
const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
|
|
449
449
|
const calls = [];
|
|
450
450
|
const res = await u.runUpgrade(
|
|
@@ -460,8 +460,11 @@ t('runUpgrade --registry (full): pins the exact version + writes the env-fed .np
|
|
|
460
460
|
assert.equal(res.ok, true, 'registry upgrade succeeds');
|
|
461
461
|
assert.equal(res.toVersion, '1.17.0');
|
|
462
462
|
assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'exact registry pin (no file:)');
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
// @bongos/core is public (task 1003872), so the registry path writes no `.npmrc` and
|
|
464
|
+
// resolves from the default registry. An `.npmrc` reappearing here would mean the retired
|
|
465
|
+
// auth plumbing came back — and it is the file whose `${NPM_TOKEN}` expansion used to
|
|
466
|
+
// SHADOW a working user-level credential (task 1002754), so its absence is load-bearing.
|
|
467
|
+
assert.equal(existsSync(join(dir, '.npmrc')), false, 'writes no .npmrc on the registry path');
|
|
465
468
|
assert.ok(calls.some((c) => /^npm install/.test(c)), 'ran npm install (resolves from the registry)');
|
|
466
469
|
assert.ok(!calls.some((c) => /vendor/.test(c)), 'did not vendor a tarball');
|
|
467
470
|
});
|
package/scripts/gds/npmrc.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
//
|
|
4
|
-
// scripts/gds/npmrc.js — env-fed `.npmrc` for installing the PRIVATE @bongos/* core
|
|
5
|
-
// from the npm registry (ADR 0134, task 2090). The single place that knows the
|
|
6
|
-
// registry host + scope + the `.npmrc` shape + the opt-in/token gates, shared by:
|
|
7
|
-
// • init.js — the greenfield `--registry-core` scaffold
|
|
8
|
-
// • provision.js — the standalone scaffold's registry path (opt-in, fail-soft)
|
|
9
|
-
// • upgrade.js — the `--registry <version>` update channel
|
|
10
|
-
//
|
|
11
|
-
// WHY private + env-fed. `@bongos/core` publishes as a SCOPED, PRIVATE package (ADR
|
|
12
|
-
// 0134): a private scoped package can only be installed with auth. Consumers
|
|
13
|
-
// authenticate with an `.npmrc` whose token EXPANDS FROM THE ENVIRONMENT at install
|
|
14
|
-
// time (`_authToken=${NPM_TOKEN}`) — so the committed file carries NO secret, and a
|
|
15
|
-
// tokenless install 404s cleanly (the expected pre-launch outcome).
|
|
16
|
-
//
|
|
17
|
-
// The vendored-tarball path (task 2053) stays the DEFAULT/fallback until this registry
|
|
18
|
-
// path is proven end-to-end on a real greenfield standup (ADR 0134 §Consequences).
|
|
19
|
-
|
|
20
|
-
const fs = require('node:fs');
|
|
21
|
-
const path = require('node:path');
|
|
22
|
-
|
|
23
|
-
const REGISTRY_HOST = 'registry.npmjs.org';
|
|
24
|
-
const CORE_SCOPE = '@bongos'; // the paid, canonical npm org (ADR 0134 scope correction, task 2092)
|
|
25
|
-
const TOKEN_ENV = 'NPM_TOKEN';
|
|
26
|
-
const REQUEST_ENV = 'PROVISION_CORE_FROM_REGISTRY';
|
|
27
|
-
|
|
28
|
-
// The `.npmrc` a scaffolded instance commits. The token is the LITERAL `${NPM_TOKEN}`
|
|
29
|
-
// (npm expands env vars in `.npmrc` values at read time), so the file is safe to
|
|
30
|
-
// commit — it holds no secret. Scoping @bongos to the registry keeps every unrelated
|
|
31
|
-
// install on the default public registry.
|
|
32
|
-
function npmrcContent({ scope = CORE_SCOPE, host = REGISTRY_HOST, tokenEnv = TOKEN_ENV } = {}) {
|
|
33
|
-
return [
|
|
34
|
-
`; Cloud Bongos — env-fed auth for the PRIVATE ${scope}/* packages (ADR 0134, task 2090).`,
|
|
35
|
-
`; The token EXPANDS FROM THE ENVIRONMENT at install time; nothing secret is committed here.`,
|
|
36
|
-
`; \`npm ci\` needs ${tokenEnv} set (a read-only npm access token from the paid \`bongos\` org);`,
|
|
37
|
-
`; without it the private package 404s — that is the expected tokenless outcome.`,
|
|
38
|
-
`${scope}:registry=https://${host}/`,
|
|
39
|
-
`//${host}/:_authToken=\${${tokenEnv}}`,
|
|
40
|
-
'',
|
|
41
|
-
].join('\n');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Is a registry token present in the environment? The fail-soft gate: provisioning
|
|
45
|
-
// falls back to the vendored tarball when this is false, so a tokenless standup still
|
|
46
|
-
// works (never a broken instance).
|
|
47
|
-
function hasNpmToken(env = process.env) {
|
|
48
|
-
return !!String(env[TOKEN_ENV] || '').trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// resolveNpmToken (task 1002754) — the install-time credential resolver that removes
|
|
52
|
-
// the hand-exported-NPM_TOKEN step from deploys.
|
|
53
|
-
//
|
|
54
|
-
// THE TRAP. The committed instance `.npmrc` reads `_authToken=${NPM_TOKEN}`; npm gives
|
|
55
|
-
// a per-project key precedence over the user-level one, so with NPM_TOKEN unset the
|
|
56
|
-
// expansion is EMPTY and it MASKS a perfectly valid literal token in ~/.npmrc — npm
|
|
57
|
-
// goes anonymous and the private core 404s. Every deploy then silently depended on the
|
|
58
|
-
// operator remembering `export NPM_TOKEN=…` (the ADR-0162 shape: a human-gated step in
|
|
59
|
-
// the ship path). npm's config syntax cannot express "env var, else fall through", so
|
|
60
|
-
// the fix is to RESOLVE the credential here and feed it to the child install's env —
|
|
61
|
-
// the committed file stays secret-free and unchanged.
|
|
62
|
-
//
|
|
63
|
-
// Order: env NPM_TOKEN wins (explicit operator intent), else the user-level ~/.npmrc's
|
|
64
|
-
// literal `//<host>/:_authToken=<value>` (ignoring env-fed `${…}` values — those are
|
|
65
|
-
// the trap, not a credential). Returns { token, source: 'env'|'user-npmrc'|null }.
|
|
66
|
-
function resolveNpmToken({ env = process.env, homedir, fsImpl = fs, host = REGISTRY_HOST } = {}) {
|
|
67
|
-
const fromEnv = String(env[TOKEN_ENV] || '').trim();
|
|
68
|
-
if (fromEnv) return { token: fromEnv, source: 'env' };
|
|
69
|
-
const home = homedir || (env.HOME || env.USERPROFILE || '');
|
|
70
|
-
if (home) {
|
|
71
|
-
try {
|
|
72
|
-
const raw = fsImpl.readFileSync(path.join(home, '.npmrc'), 'utf8');
|
|
73
|
-
const re = new RegExp(`^\\s*//${host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/:_authToken\\s*=\\s*(.+?)\\s*$`, 'm');
|
|
74
|
-
const m = re.exec(raw);
|
|
75
|
-
const val = m && m[1].replace(/^["']|["']$/g, '');
|
|
76
|
-
if (val && !val.includes('${')) return { token: val, source: 'user-npmrc' };
|
|
77
|
-
} catch { /* no user npmrc, unreadable — fall through to null */ }
|
|
78
|
-
}
|
|
79
|
-
return { token: null, source: null };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Has the operator opted the standalone scaffold into the registry path? OFF by
|
|
83
|
-
// default — the vendored tarball stays the default until the registry path is proven
|
|
84
|
-
// end-to-end (ADR 0134). '1'/'true'/'yes'/'on' (case-insensitive) → true.
|
|
85
|
-
function registryCoreRequested(env = process.env) {
|
|
86
|
-
return /^(1|true|yes|on)$/i.test(String(env[REQUEST_ENV] || '').trim());
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Write the env-fed `.npmrc` into an instance dir. Default overwrites (a greenfield
|
|
90
|
-
// scaffold owns the file); pass overwrite:false to leave a pre-existing `.npmrc`
|
|
91
|
-
// untouched (an upgrade must not clobber a consumer's other registries). Returns the
|
|
92
|
-
// path (or null when skipped). Dry-run writes nothing.
|
|
93
|
-
function writeNpmrc(dir, { fsImpl = fs, dryRun = false, overwrite = true, log = () => {} } = {}) {
|
|
94
|
-
const p = path.join(dir, '.npmrc');
|
|
95
|
-
if (!overwrite && fsImpl.existsSync(p)) {
|
|
96
|
-
log(` • .npmrc already present — left untouched (add \`${CORE_SCOPE}:registry\` + \`_authToken=\${${TOKEN_ENV}}\` if the private core doesn't resolve).`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
if (dryRun) {
|
|
100
|
-
log(` [dry-run] would write ${p} (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
|
|
101
|
-
return p;
|
|
102
|
-
}
|
|
103
|
-
fsImpl.writeFileSync(p, npmrcContent());
|
|
104
|
-
log(` ✓ wrote .npmrc (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
|
|
105
|
-
return p;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
module.exports = {
|
|
109
|
-
REGISTRY_HOST, CORE_SCOPE, TOKEN_ENV, REQUEST_ENV,
|
|
110
|
-
npmrcContent, hasNpmToken, resolveNpmToken, registryCoreRequested, writeNpmrc,
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Tiny CLI: `node scripts/gds/npmrc.js [--print | --write <dir>]` — print the `.npmrc`
|
|
114
|
-
// body or materialize it into <dir>. Handy for a manual registry-standup rehearsal.
|
|
115
|
-
if (require.main === module) {
|
|
116
|
-
const args = process.argv.slice(2);
|
|
117
|
-
const writeIdx = args.indexOf('--write');
|
|
118
|
-
if (writeIdx !== -1 && args[writeIdx + 1]) {
|
|
119
|
-
const p = writeNpmrc(path.resolve(args[writeIdx + 1]), { log: console.log });
|
|
120
|
-
process.stdout.write(`${p}\n`);
|
|
121
|
-
} else {
|
|
122
|
-
process.stdout.write(npmrcContent());
|
|
123
|
-
}
|
|
124
|
-
}
|
package/tests/npmrc.mjs
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// tests/npmrc.mjs
|
|
2
|
-
//
|
|
3
|
-
// Unit tests for scripts/gds/npmrc.js (ADR 0134, task 2090) — the single source of the
|
|
4
|
-
// env-fed `.npmrc` used to install the PRIVATE @bongos/core from the npm registry, plus
|
|
5
|
-
// the opt-in / token gates that keep the vendored-tarball path the default/fallback.
|
|
6
|
-
//
|
|
7
|
-
// Run: node tests/npmrc.mjs
|
|
8
|
-
|
|
9
|
-
import { strict as assert } from 'node:assert';
|
|
10
|
-
import { createRequire } from 'node:module';
|
|
11
|
-
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const n = require('../scripts/gds/npmrc.js');
|
|
17
|
-
|
|
18
|
-
let passed = 0, failed = 0;
|
|
19
|
-
function test(name, fn) {
|
|
20
|
-
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
21
|
-
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---- npmrcContent ----------------------------------------------------------
|
|
25
|
-
test('npmrcContent: scopes @bongos to the registry + auth token EXPANDS from the env (literal ${NPM_TOKEN})', () => {
|
|
26
|
-
const body = n.npmrcContent();
|
|
27
|
-
assert.match(body, /@bongos:registry=https:\/\/registry\.npmjs\.org\//);
|
|
28
|
-
assert.match(body, /\/\/registry\.npmjs\.org\/:_authToken=\$\{NPM_TOKEN\}/);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('npmrcContent: carries NO real secret — only the ${NPM_TOKEN} placeholder', () => {
|
|
32
|
-
const body = n.npmrcContent();
|
|
33
|
-
// the only _authToken= assignment must be the env placeholder, never a concrete value
|
|
34
|
-
const tokenLines = body.split('\n').filter((l) => /_authToken=/.test(l));
|
|
35
|
-
assert.equal(tokenLines.length, 1);
|
|
36
|
-
assert.equal(tokenLines[0].trim(), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('npmrcContent: comment lines use the ini `;` comment char', () => {
|
|
40
|
-
const comments = n.npmrcContent().split('\n').filter((l) => l.startsWith(';'));
|
|
41
|
-
assert.ok(comments.length >= 3, 'has explanatory comments');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('npmrcContent: honors custom scope/host/tokenEnv', () => {
|
|
45
|
-
const body = n.npmrcContent({ scope: '@acme', host: 'npm.acme.dev', tokenEnv: 'ACME_TOKEN' });
|
|
46
|
-
assert.match(body, /@acme:registry=https:\/\/npm\.acme\.dev\//);
|
|
47
|
-
assert.match(body, /\/\/npm\.acme\.dev\/:_authToken=\$\{ACME_TOKEN\}/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ---- gates -----------------------------------------------------------------
|
|
51
|
-
test('hasNpmToken: true only when NPM_TOKEN is a non-empty string', () => {
|
|
52
|
-
assert.equal(n.hasNpmToken({ NPM_TOKEN: 'npm_abc' }), true);
|
|
53
|
-
assert.equal(n.hasNpmToken({ NPM_TOKEN: '' }), false);
|
|
54
|
-
assert.equal(n.hasNpmToken({ NPM_TOKEN: ' ' }), false);
|
|
55
|
-
assert.equal(n.hasNpmToken({}), false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('registryCoreRequested: 1/true/yes/on → true; everything else → false', () => {
|
|
59
|
-
for (const v of ['1', 'true', 'TRUE', 'yes', 'on']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), true, v);
|
|
60
|
-
for (const v of ['0', 'false', 'no', '', 'off', 'garbage']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), false, v);
|
|
61
|
-
assert.equal(n.registryCoreRequested({}), false, 'unset');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ---- writeNpmrc ------------------------------------------------------------
|
|
65
|
-
test('writeNpmrc: writes the env-fed .npmrc + returns its path', () => {
|
|
66
|
-
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
67
|
-
try {
|
|
68
|
-
const p = n.writeNpmrc(dir, { log: () => {} });
|
|
69
|
-
assert.equal(p, join(dir, '.npmrc'));
|
|
70
|
-
assert.equal(readFileSync(p, 'utf8'), n.npmrcContent());
|
|
71
|
-
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('writeNpmrc: dry-run writes nothing but returns the intended path', () => {
|
|
75
|
-
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
76
|
-
try {
|
|
77
|
-
const p = n.writeNpmrc(dir, { dryRun: true, log: () => {} });
|
|
78
|
-
assert.equal(p, join(dir, '.npmrc'));
|
|
79
|
-
assert.equal(existsSync(join(dir, '.npmrc')), false, 'dry-run must not write');
|
|
80
|
-
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('writeNpmrc: overwrite:false leaves a pre-existing .npmrc untouched (returns null)', () => {
|
|
84
|
-
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
85
|
-
try {
|
|
86
|
-
const existing = '@other:registry=https://npm.other.dev/\n';
|
|
87
|
-
writeFileSync(join(dir, '.npmrc'), existing);
|
|
88
|
-
const p = n.writeNpmrc(dir, { overwrite: false, log: () => {} });
|
|
89
|
-
assert.equal(p, null, 'skipped');
|
|
90
|
-
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), existing, 'existing content preserved');
|
|
91
|
-
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('writeNpmrc: overwrite:true (default) replaces an existing .npmrc', () => {
|
|
95
|
-
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
96
|
-
try {
|
|
97
|
-
writeFileSync(join(dir, '.npmrc'), 'stale\n');
|
|
98
|
-
n.writeNpmrc(dir, { log: () => {} });
|
|
99
|
-
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), n.npmrcContent());
|
|
100
|
-
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ---- resolveNpmToken (task 1002754) ----------------------------------------
|
|
104
|
-
// The install-time credential resolver that removes the hand-exported-NPM_TOKEN
|
|
105
|
-
// deploy step: env wins, else the user-level ~/.npmrc's LITERAL token; env-fed
|
|
106
|
-
// `${…}` values in the user file are the trap, never a credential.
|
|
107
|
-
test('resolveNpmToken: env NPM_TOKEN wins over everything', () => {
|
|
108
|
-
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
109
|
-
try {
|
|
110
|
-
writeFileSync(join(home, '.npmrc'), '//registry.npmjs.org/:_authToken=npm_userlevel\n');
|
|
111
|
-
const r = n.resolveNpmToken({ env: { NPM_TOKEN: ' npm_fromenv ' }, homedir: home });
|
|
112
|
-
assert.deepEqual(r, { token: 'npm_fromenv', source: 'env' });
|
|
113
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('resolveNpmToken: falls back to the user-level ~/.npmrc literal token', () => {
|
|
117
|
-
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
118
|
-
try {
|
|
119
|
-
writeFileSync(join(home, '.npmrc'),
|
|
120
|
-
'registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=npm_ABC123\nfund=false\n');
|
|
121
|
-
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
122
|
-
assert.deepEqual(r, { token: 'npm_ABC123', source: 'user-npmrc' });
|
|
123
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('resolveNpmToken: an env-fed ${…} value in the user file is the trap, not a credential', () => {
|
|
127
|
-
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
128
|
-
try {
|
|
129
|
-
writeFileSync(join(home, '.npmrc'), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n');
|
|
130
|
-
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
131
|
-
assert.deepEqual(r, { token: null, source: null });
|
|
132
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('resolveNpmToken: a token for a DIFFERENT host is not borrowed', () => {
|
|
136
|
-
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
137
|
-
try {
|
|
138
|
-
writeFileSync(join(home, '.npmrc'), '//npm.pkg.github.com/:_authToken=ghp_other\n');
|
|
139
|
-
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
140
|
-
assert.deepEqual(r, { token: null, source: null });
|
|
141
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('resolveNpmToken: no user npmrc at all → null, never a throw', () => {
|
|
145
|
-
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
146
|
-
try {
|
|
147
|
-
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
148
|
-
assert.deepEqual(r, { token: null, source: null });
|
|
149
|
-
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
console.log('');
|
|
153
|
-
console.log(`npmrc tests: ${passed} passed, ${failed} failed`);
|
|
154
|
-
process.exit(failed ? 1 : 0);
|