@bongos/core 1.19.997 → 1.19.999
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +13 -13
- package/.claude/skills/read-session-export/SKILL.md +11 -35
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +14 -5
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-core-upgrade.js +127 -15
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +123 -20
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.999",
|
|
6
|
+
"core_contract": "1.19.999",
|
|
7
|
+
"source_commit": "60aa46906678b6454efb0a03da809680add5dfb0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-24T01:15:07.815Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 520,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2952,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "72dc4200fb45a7287362a518d9c090f733a2102a337dfb44e799bccede6aac6b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
{
|
|
223
223
|
"path": ".claude/skills/read-session-export/SKILL.md",
|
|
224
224
|
"mode": "0000644",
|
|
225
|
-
"sha256": "
|
|
225
|
+
"sha256": "594d32ef63be2b45c360210ad004fc14e6dae889af97d47d72aac6a738fe1878"
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
"path": ".claude/skills/recall/SKILL.md",
|
|
@@ -2132,7 +2132,7 @@
|
|
|
2132
2132
|
{
|
|
2133
2133
|
"path": "docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md",
|
|
2134
2134
|
"mode": "0000644",
|
|
2135
|
-
"sha256": "
|
|
2135
|
+
"sha256": "9ebd9e4b3cb71f3ee4c5e06cdad04471ab07988744263fef7cd40a403b296b0b"
|
|
2136
2136
|
},
|
|
2137
2137
|
{
|
|
2138
2138
|
"path": "docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md",
|
|
@@ -2717,7 +2717,7 @@
|
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/module-api-changelog.md",
|
|
2719
2719
|
"mode": "0000644",
|
|
2720
|
-
"sha256": "
|
|
2720
|
+
"sha256": "c86c94695d5011b11ecdccab22f443c99acd980521036c7137b3a4675477e285"
|
|
2721
2721
|
},
|
|
2722
2722
|
{
|
|
2723
2723
|
"path": "docs/modules-contract.md",
|
|
@@ -8187,12 +8187,12 @@
|
|
|
8187
8187
|
{
|
|
8188
8188
|
"path": "package-lock.json",
|
|
8189
8189
|
"mode": "0000644",
|
|
8190
|
-
"sha256": "
|
|
8190
|
+
"sha256": "998ead97b5036645157cf13f31f69b6d942c7734e4b333871921dc846f0a6892"
|
|
8191
8191
|
},
|
|
8192
8192
|
{
|
|
8193
8193
|
"path": "package.json",
|
|
8194
8194
|
"mode": "0000644",
|
|
8195
|
-
"sha256": "
|
|
8195
|
+
"sha256": "4048e352f641c0e350cec53aa32fcbb2f5b7396fd88d3c0a1e87c26140eec28b"
|
|
8196
8196
|
},
|
|
8197
8197
|
{
|
|
8198
8198
|
"path": "public-docs/index.html",
|
|
@@ -9147,7 +9147,7 @@
|
|
|
9147
9147
|
{
|
|
9148
9148
|
"path": "scripts/gds/provision-core-upgrade.js",
|
|
9149
9149
|
"mode": "0000644",
|
|
9150
|
-
"sha256": "
|
|
9150
|
+
"sha256": "70ee5d90d63c8b7de938bb22c7a2e49154f5e80ce7e4ac78d180148017a09e74"
|
|
9151
9151
|
},
|
|
9152
9152
|
{
|
|
9153
9153
|
"path": "scripts/gds/provision-disconnect.js",
|
|
@@ -10237,7 +10237,7 @@
|
|
|
10237
10237
|
{
|
|
10238
10238
|
"path": "src/module-api.js",
|
|
10239
10239
|
"mode": "0000644",
|
|
10240
|
-
"sha256": "
|
|
10240
|
+
"sha256": "90d976ef4d2c73ae26c2d8ea4d0db083e6105b6c385a3abcc17a8e853886975c"
|
|
10241
10241
|
},
|
|
10242
10242
|
{
|
|
10243
10243
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11152,7 +11152,7 @@
|
|
|
11152
11152
|
{
|
|
11153
11153
|
"path": "tests/core_upgrade_runner.mjs",
|
|
11154
11154
|
"mode": "0000644",
|
|
11155
|
-
"sha256": "
|
|
11155
|
+
"sha256": "104ff5419638902eb272d15040a8fe69c4e1d14c38c7a977b1c2fd64c3cadd11"
|
|
11156
11156
|
},
|
|
11157
11157
|
{
|
|
11158
11158
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: read-session-export
|
|
3
3
|
description: >-
|
|
4
|
-
Read a Claude Code /export zip — conversation, thinking,
|
|
4
|
+
Read a Claude Code /export zip — conversation, thinking, tool calls — to answer questions about a past session. READS an existing export, never creates one. Triggers: a dropped session-export-*.zip, "read/summarize this session", "what happened in this session", "what was I/you thinking", "/read-session-export".
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
The user already has the zip — Claude Code's built-in `/export` made it.
|
|
7
|
+
The user already has the zip — Claude Code's built-in `/export` made it. Read it with the reader rather than by hand: it prints a compact transcript (prompts + thinking + replies + one-line tool calls) to stdout and skips the megabytes of debug logs.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
node scripts/gds/read-session-export.js <export.zip> #
|
|
10
|
+
node scripts/gds/read-session-export.js <export.zip> # compact transcript → stdout
|
|
11
11
|
node scripts/gds/read-session-export.js --latest # newest session-export-*.zip in ~/Downloads
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Then **
|
|
14
|
+
Then **answer what the user actually asked** — summarize the session, pull out the reasoning behind a decision, find where something went wrong, list what was built. Don't paste the transcript back; that's the raw material, not the answer.
|
|
15
15
|
|
|
16
16
|
## Pick the right depth (it's fast either way)
|
|
17
17
|
|
|
@@ -22,38 +22,14 @@ Then **read that output and respond to the user's real intent** — summarize th
|
|
|
22
22
|
| Normal "read / summarize this session" | _(no flag)_ — compact transcript, tool results clipped |
|
|
23
23
|
| "I need the full tool output / exact text" | `--full` (loosens the result clipping) |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
**A long export is a file, not a dump.** Always open with `--meta`; if it reports hundreds of tool calls, redirect to your session's scratchpad directory and read it in slices, rather than pouring thousands of lines into context at once:
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- `metadata.json` — model, effort, title, cwd, branch, timestamps.
|
|
32
|
-
- `<cliSessionId>.jsonl` — **the transcript**, one JSON record per line. Assistant records carry `content[]` blocks of type `thinking` (the thoughts), `text` (the reply), and `tool_use`; the matching `tool_result` rides in the next `user` record. The reader stitches these together.
|
|
33
|
-
- `<cliSessionId>/subagents/agent-*.jsonl` — subagent transcripts (rendered after the main one).
|
|
34
|
-
- `<cliSessionId>/tool-results/*.txt` — outputs too big to inline.
|
|
35
|
-
- `logs/*.log` — app debug logs. **Not the conversation** — ignored.
|
|
36
|
-
|
|
37
|
-
You rarely need this — the reader handles it — but it's here so you can read a record by hand if you must.
|
|
38
|
-
|
|
39
|
-
## The thinking may be redacted — and what to show instead
|
|
40
|
-
|
|
41
|
-
Whether "the thoughts" are actually readable depends on the model that ran the session, not on this tool:
|
|
42
|
-
|
|
43
|
-
- **Sonnet sessions** → thinking exports as plaintext; you'll see the real reasoning.
|
|
44
|
-
- **Opus sessions** (e.g. `claude-opus-4-8`) → thinking is **redacted**: the export carries only an encrypted signature, not the words. The reader states this once in the header (`💭 0 thoughts (+N redacted)` + a one-line banner) and otherwise keeps the body clean.
|
|
45
|
-
|
|
46
|
-
**The substitute for redacted thinking is already in the transcript — not in the logs.** When the thoughts are encrypted, lean on what Claude actually *said and did*: the **text replies** routinely state the reasoning in plain language ("the problem is X, so I'll Y"), and the **tool-call sequence** shows the investigation step by step. The reader surfaces both, so you can reconstruct the "why" of a redacted session from its actions. (The zip's `logs/*.log` are NOT a fallback: they're app-wide, multi-day rolling logs with no per-session correlation and no reasoning — ignore them.)
|
|
47
|
-
|
|
48
|
-
So if the user exported an Opus session hoping to re-read the literal reasoning, tell them plainly it's redacted — then answer their question anyway from the text + actions, which usually makes the reasoning clear.
|
|
49
|
-
|
|
50
|
-
## Constraints
|
|
27
|
+
```bash
|
|
28
|
+
node scripts/gds/read-session-export.js <zip> > <scratchpad>/transcript.md # then sed -n '1,400p', '400,850p', …
|
|
29
|
+
```
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
- **Local + zero-dependency.** Pure Node stdlib (a small `zlib` ZIP reader) — no Bongos session, no network, no install. Works on any builder machine.
|
|
54
|
-
- ZIP64 archives (multi-GB) are refused with a clear message; no real session export hits that.
|
|
31
|
+
## If the thinking is redacted
|
|
55
32
|
|
|
56
|
-
|
|
33
|
+
Opus sessions export encrypted reasoning rather than words, and the reader says so in its header. Tell the user plainly — then answer their question anyway from the **text replies**, which routinely state the reasoning outright ("the problem is X, so I'll Y"), and the **tool-call sequence**, which shows the investigation step by step. The zip's `logs/*.log` are not a fallback: app-wide rolling debug logs, no per-session correlation, no reasoning.
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
- Reads: the `session-export-*.zip` you point it at. Writes: nothing.
|
|
35
|
+
Reads only — it creates no files and never modifies the zip.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR 0327 — A cloud-host runs on the owner's own account, and the key is borrowed, not stored
|
|
2
2
|
|
|
3
|
-
- **Status:** accepted
|
|
3
|
+
- **Status:** accepted — **one Context finding retracted 2026-09-23** ([task 1004186](https://cloudbongos.com/builders#/task/1004186)): `mercury` was *not* already on Render, and the first Consequences bullet that rested on it is retracted with it. Decisions 1–5 are unaffected and stand. See the correction in Context.
|
|
4
4
|
- **Date:** 2026-09-22
|
|
5
5
|
- **Task:** [task 1004175](https://cloudbongos.com/builders#/task/1004175) (goal 1000106 — working area 1, Project creation)
|
|
6
6
|
- **Deciders:** Lars (Archon) — decisions 1, 3 and 4 are the owner's. Decision 2 is Claude's design answer to the owner's question "is there a simpler way", and decision 5 records a measurement.
|
|
@@ -14,9 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
Standing that up exposed what the unexamined assumption costs. A Render API key is issued against a *user* and carries that user's rights in **every workspace they belong to**. The owner's key reached three workspaces, one of them another builder's, holding their live production services. We would have been one bug away from acting on a third party's infrastructure with a credential they never gave us.
|
|
16
16
|
|
|
17
|
-
The same check found
|
|
17
|
+
The same check found what was recorded here as a second thing: "**`mercury` was already on Render**, run by its own owner from their own repository. Under ADR 0323 we would have migrated a working project *off* a correct setup and onto our bill."
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Correction, 2026-09-23 ([task 1004186](https://cloudbongos.com/builders#/task/1004186)). That finding is wrong; the sentence above is kept only to show what was claimed.** The Bongos instance is on our own box, and always was. Measured against the live fleet and the live endpoint:
|
|
20
|
+
|
|
21
|
+
- Fleet row 31: `hosting_shape='standalone'`, `domain=mercury.cloudbongos.com`, `db_name=mercury`, `port=3018`, `onboard_mode=adopt`, `target_ref=willHermesLines/mercury_bongos`, last verified up 2026-09-24.
|
|
22
|
+
- `GET https://mercury.cloudbongos.com/healthz` → `200`, carrying the header **`via: 1.1 Caddy`** — our Caddy, our control-plane box, behind Cloudflare. `/version` reports core 1.19.312.
|
|
23
|
+
|
|
24
|
+
**What was actually seen, and why it misled.** `mercury_bongos` is a *hybrid* repository: its owner's own Python warehouse application (`app.py`, plus a `Procfile` reading `web: gunicorn app:app`) with an **adopted** Bongos instance layered onto it (`package.json` depends on `@bongos/core` as `file:vendor/bongos-core-1.19.305.tgz` — the vendoring path). It carries no `render.yaml`. So the live production services in that workspace are most plausibly the owner's *Python app* — a real Render deployment that is simply not the Bongos instance. A workspace listing and an instance row were read as the same object; they are two systems sharing a name and a repository.
|
|
25
|
+
|
|
26
|
+
That last step is the likeliest reading, not a measurement — confirming it needs the owner, or a look inside their workspace, and neither was available. What *is* measured is the half that matters: **the Bongos instance is on our box and has not moved.**
|
|
27
|
+
|
|
28
|
+
**The decision below does not depend on the retracted half.** The first finding — that a Render API key carries its user's rights into every workspace they belong to — is measured, and on its own it is sufficient. The remaining landlord relationship was never the servers; it was the **account**.
|
|
20
29
|
|
|
21
30
|
## Decision
|
|
22
31
|
|
|
@@ -86,13 +95,13 @@ So the web tier process **can** read the DigitalOcean, Cloudflare and platform t
|
|
|
86
95
|
|
|
87
96
|
## Consequences
|
|
88
97
|
|
|
89
|
-
-
|
|
98
|
+
- ~~**The migration shrinks to an adoption.** `mercury` is already in the target state; its instance record needs pointing at a deployment its owner runs, not a move.~~ **Retracted 2026-09-23 ([task 1004186](https://cloudbongos.com/builders#/task/1004186)) — see the correction in Context.** The migration does not shrink: `mercury`'s Bongos instance runs on our box and still has to move, like the other three. Because the project is someone else's, that move needs its owner's agreement and their own Render account, which makes it the courtesy call this ADR's closing thought was reaching for. <redacted> remains a real distinction — `scripts/gds/provision-repo.js` already draws it for repositories and the platform leg still needs it — but nothing in the fleet is currently an example of it, so it has to be built from the spec rather than from a live case. The pilot is [task 1004142](https://cloudbongos.com/builders#/task/1004142) and the moves are [task 1004184](https://cloudbongos.com/builders#/task/1004184).
|
|
90
99
|
|
|
91
100
|
- **The fleet cost ledger's question changes, and it is no longer "what do we owe".** [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §6 filed [task 1004148](https://cloudbongos.com/builders#/task/1004148) because `costEstimateUsd` returns 0 for a project "the platform is about to be billed for". The platform is not going to be billed for it. A ledger reporting our spend should read ~$0 per `cloud-host` and be *correct*; what an owner is worth telling is **their** estimated Render cost, which is a different column with a different meaning. Reporting one as the other is the defect now.
|
|
92
101
|
|
|
93
102
|
- **Metering and charging lose their forcing function.** [task 1001949](https://cloudbongos.com/builders#/task/1001949) was sized against recovering ~$14/project/month. There is nothing to recover. Cost *visibility* — showing an owner what their project costs them — survives as a product feature; billing for hosting does not, because we do not host.
|
|
94
103
|
|
|
95
|
-
- **`dedicated` becomes dead
|
|
104
|
+
- **`dedicated` becomes dead code, and — measured — no data at all.** Its stored value must be dropped rather than merely orphaned. Pointer corrected 2026-09-23: that work split out of task 1004142 into [task 1004182](https://cloudbongos.com/builders#/task/1004182). The same check found the fleet carries **zero** `dedicated` rows, so the "runs over live rows" caution belongs to the 22 `standalone` rows being renamed to `cloud-host`, not to `dedicated`, whose migration is a no-op. Re-verify before writing one.
|
|
96
105
|
|
|
97
106
|
- **Our own token exposure is now the largest unmitigated item in this area.** Decision 5 is filed as its own task. It was not urgent while it protected only tokens we could rotate ourselves; it stays that way, but it should not stay unowned.
|
|
98
107
|
|
|
@@ -2468,5 +2468,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2468
2468
|
landed since 1.19.995 with no explicit bump. run 35934673169. (task 1002620)
|
|
2469
2469
|
1.19.997 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2470
2470
|
landed since 1.19.996 with no explicit bump. run 35937246865. (task 1002620)
|
|
2471
|
+
1.19.998 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2472
|
+
landed since 1.19.997 with no explicit bump. run 35940192900. (task 1002620)
|
|
2473
|
+
1.19.999 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2474
|
+
landed since 1.19.998 with no explicit bump. run 35942048393. (task 1002620)
|
|
2471
2475
|
---------------------------------------------------------------------------
|
|
2472
2476
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.999",
|
|
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.999",
|
|
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.999",
|
|
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",
|
|
@@ -520,29 +520,141 @@ function parseChangeRecords(stdout) {
|
|
|
520
520
|
* is interpolated into a command is checked where it is interpolated — the rule
|
|
521
521
|
* upgradeInvocation states about the slug.
|
|
522
522
|
*/
|
|
523
|
+
/**
|
|
524
|
+
* Classify a git command that did not succeed. PURE.
|
|
525
|
+
*
|
|
526
|
+
* "COULD NOT LOOK" AND "LOOKED AND DID NOT FIND" ARE DIFFERENT ANSWERS (task 1004212).
|
|
527
|
+
* The first cut collapsed both into "could not find those releases in the core history on
|
|
528
|
+
* this machine", and that sentence sent the investigation at the history for an hour when
|
|
529
|
+
* the history was never the problem — the directory was. A message that names the wrong
|
|
530
|
+
* cause is worse than a vague one, because it is followed.
|
|
531
|
+
*/
|
|
532
|
+
function gitFault(r, dir) {
|
|
533
|
+
const said = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
534
|
+
if (/dubious ownership|safe\.directory/i.test(said)) {
|
|
535
|
+
return `the core checkout at ${dir} belongs to another account, so this could not read it`;
|
|
536
|
+
}
|
|
537
|
+
if (/not a git repository/i.test(said)) return `${dir} is not a git checkout`;
|
|
538
|
+
const tail = said.trim().split('\n').filter(Boolean).slice(-1)[0] || '';
|
|
539
|
+
return `could not read ${dir}${tail ? ` — ${tail.slice(0, 160)}` : ''}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Where the CORE's own git history lives on this machine, if anywhere (task 1004212).
|
|
544
|
+
*
|
|
545
|
+
* WHAT THE FIRST CUT GOT WRONG, and it was not a small thing. It read `selfInstanceRoot()`
|
|
546
|
+
* on the assumption that the control plane's instance root is a checkout of the core. On
|
|
547
|
+
* the live box it is not: `/version` there reports a commit that does not exist in the core
|
|
548
|
+
* repository at all, because the instance root is a checkout of the INSTANCE's own repo
|
|
549
|
+
* with the core vendored in at node_modules (the task 1004123 shape). So every lookup
|
|
550
|
+
* searched the wrong repository's history.
|
|
551
|
+
*
|
|
552
|
+
* THAT IS A CORRECTNESS BUG, NOT ONLY A MISSING FEATURE. On a box whose instance repo
|
|
553
|
+
* happened to carry `Merge <branch>: <summary>` commits — which any repo built this way
|
|
554
|
+
* does — the page would have shown ANOTHER project's changelog as the core's, with no
|
|
555
|
+
* error at all. Which is why a candidate is not trusted for being a git repo: it has to
|
|
556
|
+
* prove it carries core RELEASE history, and `looksLikeCore` is that proof.
|
|
557
|
+
*
|
|
558
|
+
* NO PATH IS HARDCODED. `audit-authorship.sh` forbids naming the founder's directories,
|
|
559
|
+
* and a guessed path is how this got here. The candidates are: an explicit operator
|
|
560
|
+
* setting, the directory the CLI is executing from, and the instance root — each checked,
|
|
561
|
+
* none assumed.
|
|
562
|
+
*/
|
|
563
|
+
function coreCheckoutCandidates(deps) {
|
|
564
|
+
const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
|
|
565
|
+
const env = deps.env || process.env;
|
|
566
|
+
const out = [];
|
|
567
|
+
const add = (dir, why) => { if (dir && !out.some((c) => c.dir === dir)) out.push({ dir, why }); };
|
|
568
|
+
add(String(env.PROVISION_CORE_CHECKOUT || '').trim() || null, 'PROVISION_CORE_CHECKOUT');
|
|
569
|
+
add(REPO_ROOT, 'the directory this core is running from');
|
|
570
|
+
add(selfRoot(), "this project's own root");
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Read the core's release history, or say precisely why it cannot be. Returns
|
|
576
|
+
* `{ dir }` on success, or `{ error }` — never a directory it has not verified.
|
|
577
|
+
*/
|
|
578
|
+
function findCoreCheckout(deps, git) {
|
|
579
|
+
const tried = [];
|
|
580
|
+
for (const { dir, why } of coreCheckoutCandidates(deps)) {
|
|
581
|
+
const probe = git(dir, 'rev-parse --is-shallow-repository');
|
|
582
|
+
if (!probe.ok) { tried.push(`${why}: ${gitFault(probe, dir)}`); continue; }
|
|
583
|
+
if (probe.stdout.trim() === 'true') {
|
|
584
|
+
// A depth-limited clone resolves HEAD perfectly well — which is why /version reports
|
|
585
|
+
// `resolvedVia: git` on a box that cannot answer this — and carries no old releases.
|
|
586
|
+
tried.push(`${why}: ${dir} is a shallow clone, so it carries no release history`);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
// THE PROOF THAT THIS IS THE CORE. Any repository answers `rev-parse`; only the core's
|
|
590
|
+
// carries release commits, and accepting a repo without asking is how the wrong
|
|
591
|
+
// project's changelog would have been shown as this one's.
|
|
592
|
+
const marker = git(dir, "log --format=%H --max-count=1 -E --grep='^release: core [0-9]+\\.[0-9]+\\.[0-9]+ '");
|
|
593
|
+
if (!marker.ok) { tried.push(`${why}: ${gitFault(marker, dir)}`); continue; }
|
|
594
|
+
if (!marker.stdout.trim()) { tried.push(`${why}: ${dir} is a git checkout, but not of the core`); continue; }
|
|
595
|
+
return { dir };
|
|
596
|
+
}
|
|
597
|
+
return { error: `no core checkout is reachable on this machine (${tried.join('; ')}). Set PROVISION_CORE_CHECKOUT to one.` };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* What landed between two core releases, as the summaries their ships wrote.
|
|
602
|
+
*
|
|
603
|
+
* Returns `{ from, to, entries, more }`, or `{ from, to, error }` — NEVER an empty
|
|
604
|
+
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
605
|
+
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
606
|
+
* must not give, and it is exactly what a missing checkout produces.
|
|
607
|
+
*
|
|
608
|
+
* Every version reaching a shell is re-checked against the strict three-number form
|
|
609
|
+
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
610
|
+
* is interpolated into a command is checked where it is interpolated — the rule
|
|
611
|
+
* upgradeInvocation states about the slug.
|
|
612
|
+
*/
|
|
523
613
|
function changesBetween(fromVersion, toVersion, deps) {
|
|
524
614
|
const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
|
|
525
615
|
if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
|
|
526
|
-
// The CONTROL PLANE's executor
|
|
527
|
-
//
|
|
528
|
-
//
|
|
616
|
+
// The CONTROL PLANE's executor, never the previewed instance's: under
|
|
617
|
+
// PROVISION_REMOTE_HOST `exec` is the remote box, while the core history — and the whole
|
|
618
|
+
// question of what changed in the CORE — belongs to the machine that builds it.
|
|
529
619
|
const exec = deps.controlExec || deps.exec;
|
|
530
|
-
|
|
531
|
-
|
|
620
|
+
// AS THE ACCOUNT THAT OWNS THE CHECKOUT, for the same reason the dry run beside this one
|
|
621
|
+
// does (task 1004128). `appUser()` is defined as "the unprivileged account that owns this
|
|
622
|
+
// checkout" and throws rather than return root, so a runner executing as root reads the
|
|
623
|
+
// files through the account that may actually read them instead of tripping git's
|
|
624
|
+
// ownership check. Unprivileged runners are unchanged — there is nothing to escalate to.
|
|
625
|
+
let runAs = null;
|
|
626
|
+
if (deps.privileged) {
|
|
627
|
+
try { runAs = (deps.appUser || appUser)(); } catch { runAs = null; }
|
|
628
|
+
}
|
|
629
|
+
const git = (dir, args) => {
|
|
630
|
+
const r = exec(`${runAs ? `sudo -u ${runAs} ` : ''}git -C ${dir} ${args}`, { allowFail: true });
|
|
631
|
+
return {
|
|
632
|
+
ok: !!(r && r.softFailed !== true && r.ok !== false),
|
|
633
|
+
stdout: String((r && r.stdout) || ''),
|
|
634
|
+
stderr: String((r && r.stderr) || ''),
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
const found = findCoreCheckout(deps, git);
|
|
638
|
+
if (found.error) return { from: fromVersion, to: toVersion, error: found.error };
|
|
639
|
+
const dir = found.dir;
|
|
532
640
|
const shaOf = (v) => {
|
|
533
|
-
const r =
|
|
534
|
-
|
|
535
|
-
|
|
641
|
+
const r = git(dir, `log --format=%H --max-count=1 -F --grep='release: core ${v} '`);
|
|
642
|
+
if (!r.ok) return { fault: gitFault(r, dir) };
|
|
643
|
+
const sha = r.stdout.trim().split('\n')[0] || '';
|
|
644
|
+
return /^[0-9a-f]{7,40}$/.test(sha) ? { sha } : {};
|
|
536
645
|
};
|
|
537
646
|
const from = shaOf(fromVersion);
|
|
538
647
|
const to = shaOf(toVersion);
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
648
|
+
if (from.fault || to.fault) return { from: fromVersion, to: toVersion, error: from.fault || to.fault };
|
|
649
|
+
// LOOKED, AND DID NOT FIND — a different sentence from "could not look", and it names
|
|
650
|
+
// WHICH version is missing so the reader can tell a not-yet-published target from a
|
|
651
|
+
// checkout that has not been fetched.
|
|
652
|
+
const missing = [!from.sha && fromVersion, !to.sha && toVersion].filter(Boolean);
|
|
653
|
+
if (missing.length) {
|
|
654
|
+
return { from: fromVersion, to: toVersion, error: `core ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not in the core history on this machine yet` };
|
|
545
655
|
}
|
|
656
|
+
const r = git(dir, `log --merges --format=%s%x1f%b%x1e ${from.sha}..${to.sha}`);
|
|
657
|
+
if (!r.ok) return { from: fromVersion, to: toVersion, error: gitFault(r, dir) };
|
|
546
658
|
const all = parseChangeRecords(r.stdout);
|
|
547
659
|
return { from: fromVersion, to: toVersion, entries: all.slice(0, CHANGE_CAP), more: Math.max(0, all.length - CHANGE_CAP) };
|
|
548
660
|
}
|
|
@@ -668,4 +780,4 @@ async function snapshotAllCoreVersions(deps) {
|
|
|
668
780
|
return { seen };
|
|
669
781
|
}
|
|
670
782
|
|
|
671
|
-
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, changesBetween, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
783
|
+
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
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.999'; // 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');
|
|
@@ -883,16 +883,36 @@ await t('a paragraph-long summary is bounded — it reaches a DOM', () => {
|
|
|
883
883
|
|
|
884
884
|
// --- the range, and every way it can fail to resolve ------------------------------
|
|
885
885
|
|
|
886
|
-
|
|
886
|
+
// A box that really does hold a core checkout. `repos` maps a directory to what git
|
|
887
|
+
// there answers: `core` (a real core checkout), `notCore` (a git repo of something else),
|
|
888
|
+
// `shallow`, `owned` (git refuses on ownership) or absent (not a git repo).
|
|
889
|
+
const changeDeps = ({
|
|
890
|
+
shas = { '1.19.900': 'a'.repeat(40), '1.19.949': 'b'.repeat(40) },
|
|
891
|
+
log = LOG, logOk = true, repos = { '/srv/core': 'core' }, env = {}, privileged = false,
|
|
892
|
+
} = {}) => {
|
|
887
893
|
const ran = [];
|
|
888
894
|
return {
|
|
889
|
-
ran,
|
|
895
|
+
ran, env, privileged,
|
|
890
896
|
selfInstanceRoot: () => '/srv/core',
|
|
897
|
+
appUser: () => 'appacct',
|
|
891
898
|
exec: (cmd) => {
|
|
892
899
|
ran.push(cmd);
|
|
893
|
-
const
|
|
900
|
+
const dir = (/git -C (\S+) /.exec(cmd) || [])[1];
|
|
901
|
+
const kind = repos[dir];
|
|
902
|
+
if (!kind) return { ok: false, softFailed: true, stdout: '', stderr: `fatal: not a git repository: '${dir}'` };
|
|
903
|
+
if (kind === 'owned') {
|
|
904
|
+
return { ok: false, softFailed: true, stdout: '', stderr: `fatal: detected dubious ownership in repository at '${dir}'` };
|
|
905
|
+
}
|
|
906
|
+
if (/rev-parse --is-shallow-repository/.test(cmd)) {
|
|
907
|
+
return { ok: true, stdout: `${kind === 'shallow' ? 'true' : 'false'}\n` };
|
|
908
|
+
}
|
|
909
|
+
// The marker probe: does this repo carry core RELEASE commits at all?
|
|
910
|
+
if (/-E --grep='\^release: core/.test(cmd)) {
|
|
911
|
+
return { ok: true, stdout: kind === 'core' ? `${'c'.repeat(40)}\n` : '\n' };
|
|
912
|
+
}
|
|
913
|
+
const m = /-F --grep='release: core ([0-9.]+) '/.exec(cmd);
|
|
894
914
|
if (m) return { ok: true, stdout: `${shas[m[1]] || ''}\n` };
|
|
895
|
-
if (!logOk) return { ok: false, softFailed: true, stdout: '' };
|
|
915
|
+
if (!logOk) return { ok: false, softFailed: true, stdout: '', stderr: 'fatal: bad revision' };
|
|
896
916
|
return { ok: true, stdout: log };
|
|
897
917
|
},
|
|
898
918
|
};
|
|
@@ -908,29 +928,85 @@ await t('the range resolves through the release commits and lists what landed be
|
|
|
908
928
|
assert.ok(d.ran.some((c) => /--merges/.test(c) && /a{40}\.\.b{40}/.test(c)), 'the log is asked for the resolved range');
|
|
909
929
|
});
|
|
910
930
|
|
|
911
|
-
await t('IT
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
// PROVISION_REMOTE_HOST `exec` is the remote box, so the executor matters too.
|
|
931
|
+
await t('IT USES THE CONTROL PLANE’S EXECUTOR, whichever instance is previewed', () => {
|
|
932
|
+
// Under PROVISION_REMOTE_HOST `exec` is the remote box, while the core history belongs
|
|
933
|
+
// to the machine that builds the core.
|
|
915
934
|
const d = changeDeps();
|
|
916
935
|
d.controlExec = d.exec;
|
|
917
936
|
d.exec = () => { throw new Error('the instance executor must not be used for core history'); };
|
|
937
|
+
assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 2);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// ---- WHICH checkout, and the bug that made this task (task 1004212) ---------------
|
|
941
|
+
|
|
942
|
+
await t('A GIT REPO THAT IS NOT THE CORE IS REFUSED, not read as if it were', () => {
|
|
943
|
+
// THE BUG THIS TASK EXISTS FOR. The first cut trusted selfInstanceRoot() to be a core
|
|
944
|
+
// checkout. On the live box it is the INSTANCE's own repo with the core vendored in at
|
|
945
|
+
// node_modules — its /version commit does not exist in the core repository at all. Any
|
|
946
|
+
// repo built that way carries `Merge <branch>: <summary>` commits, so without this the
|
|
947
|
+
// page would have shown ANOTHER project's changelog as the core's, with no error.
|
|
948
|
+
const d = changeDeps({ repos: { '/srv/core': 'notCore' } });
|
|
918
949
|
const r = R.changesBetween('1.19.900', '1.19.949', d);
|
|
919
|
-
assert.equal(r.entries
|
|
920
|
-
|
|
950
|
+
assert.equal(r.entries, undefined, 'nothing is listed from the wrong repository');
|
|
951
|
+
assert.match(r.error, /not of the core/);
|
|
952
|
+
assert.ok(!d.ran.some((c) => /--merges/.test(c)), 'and its log is never even read');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
await t('the candidates are tried in order, and an operator setting wins', () => {
|
|
956
|
+
const d = changeDeps({
|
|
957
|
+
repos: { '/opt/corerepo': 'core', '/srv/core': 'core' },
|
|
958
|
+
env: { PROVISION_CORE_CHECKOUT: '/opt/corerepo' },
|
|
959
|
+
});
|
|
960
|
+
assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 2);
|
|
961
|
+
assert.ok(d.ran.every((c) => /git -C \/opt\/corerepo /.test(c)), 'only the named checkout is touched');
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
await t('a candidate that is not a core checkout FALLS THROUGH to one that is', () => {
|
|
965
|
+
const d = changeDeps({
|
|
966
|
+
repos: { '/opt/notcore': 'notCore', '/srv/core': 'core' },
|
|
967
|
+
env: { PROVISION_CORE_CHECKOUT: '/opt/notcore' },
|
|
968
|
+
});
|
|
969
|
+
assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 2);
|
|
970
|
+
assert.ok(d.ran.some((c) => /git -C \/srv\/core /.test(c)), 'the next candidate is reached');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
await t('NO reachable core checkout names every candidate it tried, and the way out', () => {
|
|
974
|
+
const d = changeDeps({ repos: {} });
|
|
975
|
+
const r = R.changesBetween('1.19.900', '1.19.949', d);
|
|
976
|
+
assert.equal(r.entries, undefined);
|
|
977
|
+
assert.match(r.error, /no core checkout is reachable/);
|
|
978
|
+
assert.match(r.error, /PROVISION_CORE_CHECKOUT/, 'and the setting that fixes it');
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// ---- "could not look" vs "looked and did not find" -------------------------------
|
|
982
|
+
|
|
983
|
+
await t('AN OWNERSHIP REFUSAL SAYS SO — it is not a missing release', () => {
|
|
984
|
+
// The misdiagnosis this replaces: every one of these used to print "could not find
|
|
985
|
+
// those releases in the core history", which sends the reader at the history when the
|
|
986
|
+
// history was never the problem.
|
|
987
|
+
const d = changeDeps({ repos: { '/srv/core': 'owned' } });
|
|
988
|
+
const r = R.changesBetween('1.19.900', '1.19.949', d);
|
|
989
|
+
assert.match(r.error, /belongs to another account/);
|
|
990
|
+
assert.doesNotMatch(r.error, /not in the core history/);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
await t('a shallow clone says so — it resolves HEAD fine and carries no releases', () => {
|
|
994
|
+
const d = changeDeps({ repos: { '/srv/core': 'shallow' } });
|
|
995
|
+
assert.match(R.changesBetween('1.19.900', '1.19.949', d).error, /shallow clone/);
|
|
921
996
|
});
|
|
922
997
|
|
|
923
|
-
await t('
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
assert.
|
|
929
|
-
|
|
998
|
+
await t('a release the checkout has not fetched NAMES which one', () => {
|
|
999
|
+
const d = changeDeps();
|
|
1000
|
+
const r = R.changesBetween('1.19.900', '9.9.9', d);
|
|
1001
|
+
assert.equal(r.entries, undefined, 'no list at all');
|
|
1002
|
+
assert.match(r.error, /core 9\.9\.9 is not in the core history on this machine yet/);
|
|
1003
|
+
assert.doesNotMatch(r.error, /could not read/, 'it looked successfully — it just did not find it');
|
|
1004
|
+
});
|
|
930
1005
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
assert.
|
|
1006
|
+
await t('a log that fails after the range resolved is reported as a read failure', () => {
|
|
1007
|
+
const r = R.changesBetween('1.19.900', '1.19.949', changeDeps({ logOk: false }));
|
|
1008
|
+
assert.equal(r.entries, undefined);
|
|
1009
|
+
assert.match(r.error, /could not read \/srv\/core/);
|
|
934
1010
|
});
|
|
935
1011
|
|
|
936
1012
|
await t('a version that is not three numbers never reaches the shell', () => {
|
|
@@ -941,6 +1017,33 @@ await t('a version that is not three numbers never reaches the shell', () => {
|
|
|
941
1017
|
assert.match(R.changesBetween(null, '1.19.949', changeDeps()).error, /could not work out/, 'an unknown starting point too');
|
|
942
1018
|
});
|
|
943
1019
|
|
|
1020
|
+
// ---- as WHICH account ------------------------------------------------------------
|
|
1021
|
+
|
|
1022
|
+
await t('a privileged runner reads git as the account that owns the checkout', () => {
|
|
1023
|
+
// The same move the dry run beside this one makes (task 1004128). appUser() is defined
|
|
1024
|
+
// as "the unprivileged account that owns this checkout", so a runner executing as root
|
|
1025
|
+
// reads through an account that may actually read the files.
|
|
1026
|
+
const d = changeDeps({ privileged: true });
|
|
1027
|
+
R.changesBetween('1.19.900', '1.19.949', d);
|
|
1028
|
+
assert.ok(d.ran.length > 0);
|
|
1029
|
+
for (const c of d.ran) assert.match(c, /^sudo -u appacct git -C /, `every read is escalated: ${c}`);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
await t('an UNPRIVILEGED runner escalates nothing — there is nothing to escalate to', () => {
|
|
1033
|
+
const d = changeDeps();
|
|
1034
|
+
R.changesBetween('1.19.900', '1.19.949', d);
|
|
1035
|
+
for (const c of d.ran) assert.doesNotMatch(c, /sudo/);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
await t('an app user that cannot be resolved does not take the whole read down', () => {
|
|
1039
|
+
// appUser() THROWS when it would return root or an unusable name. That is a reason to
|
|
1040
|
+
// read unescalated and let git say whether it could, not a reason to return no answer.
|
|
1041
|
+
const d = changeDeps({ privileged: true });
|
|
1042
|
+
d.appUser = () => { throw new Error('the app user "root" is root'); };
|
|
1043
|
+
assert.equal(R.changesBetween('1.19.900', '1.19.949', d).entries.length, 2);
|
|
1044
|
+
for (const c of d.ran) assert.doesNotMatch(c, /sudo/);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
944
1047
|
await t('a long range is capped, and says how many it did not list', () => {
|
|
945
1048
|
const many = Array.from({ length: R.CHANGE_CAP + 5 }, (_, i) => REC(`Merge b-${i}: summary number ${i}`)).join('');
|
|
946
1049
|
const r = R.changesBetween('1.19.900', '1.19.949', changeDeps({ log: many }));
|