@bongos/core 1.19.679 → 1.19.681
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 +33 -23
- package/.gitattributes +16 -2
- package/clients/bongos-client/index.d.ts +1 -2
- package/docs/adr/0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md +48 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -29
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/instance-service-restart.md +89 -0
- package/modules/lifecycle/github-push.js +23 -5
- package/modules/lifecycle/routes/tasks.js +36 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/git-merge-regen.js +29 -0
- package/scripts/gds/upgrade.js +153 -17
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +5 -0
- package/tests/git_merge_regen.mjs +64 -2
- package/tests/publish_branch_route.mjs +57 -0
- package/tests/upgrade.mjs +259 -0
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.681",
|
|
6
|
+
"core_contract": "1.19.681",
|
|
7
|
+
"source_commit": "e31ae913044e78d0b2b8202995eb551dad418a9e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-12T02:05:45.659Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 476,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 2124,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2624,
|
|
20
|
+
"tree_sha256": "f249c4df05688bdb744709b348940982241ccc686471029262564d9b79bdd140",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
{
|
|
323
323
|
"path": ".gitattributes",
|
|
324
324
|
"mode": "0000644",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "72bc38b553e34bc652f0b5b5b2ab24999b4b11c2d6fc3699489e157fd0127a11"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "CLAUDE.md",
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.d.ts",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "101095af6616b30d3338ec12394343f360d8a583ef3da84f0dce49edc80630cb"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1904,10 +1904,15 @@
|
|
|
1904
1904
|
"mode": "0000644",
|
|
1905
1905
|
"sha256": "3d941c0e1f1ce5618fa6861a720a4ac1c1988495c5f17aed77a348a4a3b7b718"
|
|
1906
1906
|
},
|
|
1907
|
+
{
|
|
1908
|
+
"path": "docs/adr/0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md",
|
|
1909
|
+
"mode": "0000644",
|
|
1910
|
+
"sha256": "46e65d06711ad356563b01b824c416d03cecf135f9f69a60e469710e91cb3f62"
|
|
1911
|
+
},
|
|
1907
1912
|
{
|
|
1908
1913
|
"path": "docs/adr/README.md",
|
|
1909
1914
|
"mode": "0000644",
|
|
1910
|
-
"sha256": "
|
|
1915
|
+
"sha256": "6766d2b792da65e6cebc79adf4261b710d6f5ce113a8a84953cf1c643e20a0b7"
|
|
1911
1916
|
},
|
|
1912
1917
|
{
|
|
1913
1918
|
"path": "docs/api-reference.md",
|
|
@@ -1917,7 +1922,7 @@
|
|
|
1917
1922
|
{
|
|
1918
1923
|
"path": "docs/api/openapi.json",
|
|
1919
1924
|
"mode": "0000644",
|
|
1920
|
-
"sha256": "
|
|
1925
|
+
"sha256": "b09110e4eec5225c0594e4f7a3bd3e18878f751ee4697af87209568d69483c3a"
|
|
1921
1926
|
},
|
|
1922
1927
|
{
|
|
1923
1928
|
"path": "docs/architecture.md",
|
|
@@ -2792,7 +2797,7 @@
|
|
|
2792
2797
|
{
|
|
2793
2798
|
"path": "docs/module-api-changelog.md",
|
|
2794
2799
|
"mode": "0000644",
|
|
2795
|
-
"sha256": "
|
|
2800
|
+
"sha256": "244cffb195592c644ad06262ed1a0f610e7744fd62e634a35c5cddd954c9def6"
|
|
2796
2801
|
},
|
|
2797
2802
|
{
|
|
2798
2803
|
"path": "docs/modules-contract.md",
|
|
@@ -2949,6 +2954,11 @@
|
|
|
2949
2954
|
"mode": "0000644",
|
|
2950
2955
|
"sha256": "259148d6e612209502b3f46e82e57c62da049fac4d883755e151be37de92fbf7"
|
|
2951
2956
|
},
|
|
2957
|
+
{
|
|
2958
|
+
"path": "docs/recipes/instance-service-restart.md",
|
|
2959
|
+
"mode": "0000644",
|
|
2960
|
+
"sha256": "77b4f5e695ee839b6e9f7e192af6f13114069a114cc2f63167582a4a8768f4ce"
|
|
2961
|
+
},
|
|
2952
2962
|
{
|
|
2953
2963
|
"path": "docs/recipes/local-dev.md",
|
|
2954
2964
|
"mode": "0000644",
|
|
@@ -5722,7 +5732,7 @@
|
|
|
5722
5732
|
{
|
|
5723
5733
|
"path": "modules/lifecycle/github-push.js",
|
|
5724
5734
|
"mode": "0000644",
|
|
5725
|
-
"sha256": "
|
|
5735
|
+
"sha256": "1a84e2225c58969b7bc6c43643921278966c78a6049488deb6c39048982ae005"
|
|
5726
5736
|
},
|
|
5727
5737
|
{
|
|
5728
5738
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -5937,7 +5947,7 @@
|
|
|
5937
5947
|
{
|
|
5938
5948
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
5939
5949
|
"mode": "0000644",
|
|
5940
|
-
"sha256": "
|
|
5950
|
+
"sha256": "456b1d53d2349a499e1b07897dc6e16c72d26156a71347382c5a2508dc94d28d"
|
|
5941
5951
|
},
|
|
5942
5952
|
{
|
|
5943
5953
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -7767,12 +7777,12 @@
|
|
|
7767
7777
|
{
|
|
7768
7778
|
"path": "package-lock.json",
|
|
7769
7779
|
"mode": "0000644",
|
|
7770
|
-
"sha256": "
|
|
7780
|
+
"sha256": "fd92e4204b64289d8404adb924c9142b47c10b0a6c73d709583cbbad01cd6e0a"
|
|
7771
7781
|
},
|
|
7772
7782
|
{
|
|
7773
7783
|
"path": "package.json",
|
|
7774
7784
|
"mode": "0000644",
|
|
7775
|
-
"sha256": "
|
|
7785
|
+
"sha256": "242ae038fbdb23f8d375f04e794c87100df9df4c250b93942ca4eaed82017ca3"
|
|
7776
7786
|
},
|
|
7777
7787
|
{
|
|
7778
7788
|
"path": "public-docs/index.html",
|
|
@@ -8322,7 +8332,7 @@
|
|
|
8322
8332
|
{
|
|
8323
8333
|
"path": "scripts/gds/git-merge-regen.js",
|
|
8324
8334
|
"mode": "0000644",
|
|
8325
|
-
"sha256": "
|
|
8335
|
+
"sha256": "9896dc201aaa7186838fe4dcecf5226fdb0736b1b7094a98649e2f8fcbfc4d1a"
|
|
8326
8336
|
},
|
|
8327
8337
|
{
|
|
8328
8338
|
"path": "scripts/gds/go-live.js",
|
|
@@ -9102,7 +9112,7 @@
|
|
|
9102
9112
|
{
|
|
9103
9113
|
"path": "scripts/gds/upgrade.js",
|
|
9104
9114
|
"mode": "0000644",
|
|
9105
|
-
"sha256": "
|
|
9115
|
+
"sha256": "642d0ade55c203d6b07478a07d3c21c26fa4635dc8a92e4fe989434e635abbbb"
|
|
9106
9116
|
},
|
|
9107
9117
|
{
|
|
9108
9118
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -9532,7 +9542,7 @@
|
|
|
9532
9542
|
{
|
|
9533
9543
|
"path": "src/module-api.js",
|
|
9534
9544
|
"mode": "0000644",
|
|
9535
|
-
"sha256": "
|
|
9545
|
+
"sha256": "b06c931aa58a7997f7733e13d2850d290540647c4a7d243cc07d27c20d5b812e"
|
|
9536
9546
|
},
|
|
9537
9547
|
{
|
|
9538
9548
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10202,7 +10212,7 @@
|
|
|
10202
10212
|
{
|
|
10203
10213
|
"path": "tests/conflict_resolve.mjs",
|
|
10204
10214
|
"mode": "0000644",
|
|
10205
|
-
"sha256": "
|
|
10215
|
+
"sha256": "e36410c3288ec8c38c22bd5fd285d7abc6e0cadc4433ce87028bb7f9c7d54806"
|
|
10206
10216
|
},
|
|
10207
10217
|
{
|
|
10208
10218
|
"path": "tests/connections_api.mjs",
|
|
@@ -10632,7 +10642,7 @@
|
|
|
10632
10642
|
{
|
|
10633
10643
|
"path": "tests/git_merge_regen.mjs",
|
|
10634
10644
|
"mode": "0000644",
|
|
10635
|
-
"sha256": "
|
|
10645
|
+
"sha256": "52f02bc7f0da86b29d4b39da78a34f9677f7bdd93e24a4e5365ffb1af64fc413"
|
|
10636
10646
|
},
|
|
10637
10647
|
{
|
|
10638
10648
|
"path": "tests/github_push_ancestry.mjs",
|
|
@@ -12182,7 +12192,7 @@
|
|
|
12182
12192
|
{
|
|
12183
12193
|
"path": "tests/publish_branch_route.mjs",
|
|
12184
12194
|
"mode": "0000644",
|
|
12185
|
-
"sha256": "
|
|
12195
|
+
"sha256": "f688f5da9bfc392ce203ee3b89dfa9ba23fd66ca4ebac603f6fb6faedb83b5c7"
|
|
12186
12196
|
},
|
|
12187
12197
|
{
|
|
12188
12198
|
"path": "tests/publish_gate.mjs",
|
|
@@ -12977,7 +12987,7 @@
|
|
|
12977
12987
|
{
|
|
12978
12988
|
"path": "tests/upgrade.mjs",
|
|
12979
12989
|
"mode": "0000644",
|
|
12980
|
-
"sha256": "
|
|
12990
|
+
"sha256": "106735a9ab28b4a23e2f25269030d982798da3fa39e31d40a1ec78277ac971bc"
|
|
12981
12991
|
},
|
|
12982
12992
|
{
|
|
12983
12993
|
"path": "tests/upgrade_persist_pin.mjs",
|
package/.gitattributes
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*.bash text eol=lf
|
|
19
19
|
.husky/pre-commit text eol=lf
|
|
20
20
|
.husky/pre-push text eol=lf
|
|
21
|
+
.husky/post-merge text eol=lf
|
|
21
22
|
|
|
22
23
|
# Node / Python sources: keep LF everywhere too. Node tolerates CRLF, but LF
|
|
23
24
|
# keeps diffs clean and avoids surprises when these are sourced or piped.
|
|
@@ -57,8 +58,13 @@
|
|
|
57
58
|
# publish-reconciler runs a temp-clone merge with this driver registered, so a PR
|
|
58
59
|
# that conflicts only on these files lands without a human (GitHub's own auto-merge
|
|
59
60
|
# can't run a custom driver, hence the server does the merge locally then pushes).
|
|
60
|
-
# NB: the TOP-LEVEL CLAUDE.md
|
|
61
|
-
#
|
|
61
|
+
# NB (task 1001431): the TOP-LEVEL CLAUDE.md IS listed now. It used to be excluded as
|
|
62
|
+
# "hand-written", and that was true until ADR 0062 §8 gave its §13 a generated
|
|
63
|
+
# BEGIN/END SESSION-LOG SNIPPET block — after which two sessions each adding a session-log
|
|
64
|
+
# file conflicted there on every merge with nothing to heal it. It is the same shape as
|
|
65
|
+
# docs/file-map.md (hand-written prose + a generated block), not the whole-file shape, so
|
|
66
|
+
# the driver 3-way-merges the prose and re-renders only the snippet; a genuine conflict in
|
|
67
|
+
# the prose still falls through to a normal git conflict. scripts/gds/CLAUDE.md
|
|
62
68
|
# LEFT that set in task 1003548 (its ~40k-char block was 76% of a file loaded on every
|
|
63
69
|
# visit to that directory; docs/repo-map.md still carries the full index), so its entry
|
|
64
70
|
# came out with it — a path listed here whose generator no longer owns it would route a
|
|
@@ -72,6 +78,14 @@
|
|
|
72
78
|
# appear in a merge and these two driver entries never fire. They are KEPT — harmless —
|
|
73
79
|
# as a transition backstop for any in-flight branch that still carries a committed copy;
|
|
74
80
|
# once none remain they can be dropped. The other four entries stay load-bearing.
|
|
81
|
+
# The LEADING SLASH is load-bearing: a gitattributes pattern with no slash matches at ANY
|
|
82
|
+
# depth, so a bare `CLAUDE.md` would also capture all 22 modules/*/CLAUDE.md and
|
|
83
|
+
# scripts/gds/CLAUDE.md — files the driver does NOT know (they are not in NESTED_DIRS, so
|
|
84
|
+
# REGENERABLE has no rule for them). The driver would then exit 1 on each and FORCE a
|
|
85
|
+
# conflict git would otherwise have merged cleanly — the exact 'routed through a driver
|
|
86
|
+
# that cannot regenerate it' failure this file warns about above. `/CLAUDE.md` anchors to
|
|
87
|
+
# the repo root. Verified with `git check-attr merge -- modules/economy/CLAUDE.md`.
|
|
88
|
+
/CLAUDE.md merge=otb-regen
|
|
75
89
|
docs/repo-map.md merge=otb-regen
|
|
76
90
|
docs/file-map.md merge=otb-regen
|
|
77
91
|
docs/session-log-index.md merge=otb-regen
|
|
@@ -384,7 +384,6 @@ export interface PostTasksIdMergeRequest { into_task_id: number; reason?: string
|
|
|
384
384
|
export interface PostTasksIdOverrideRequestRequest { rationale: string }
|
|
385
385
|
export interface PostTasksIdOverrideRequestResponse { ok: boolean; request: unknown }
|
|
386
386
|
export interface PostTasksIdPublishBranchRequest { branch: string; head_sha: string; bundle_b64: string; value_summary?: string }
|
|
387
|
-
export interface PostTasksIdPublishBranchResponse { ok: boolean; pr_url: unknown; pr_number: unknown; head_sha: unknown; auto_merge: unknown }
|
|
388
387
|
export interface PostTasksIdShipResponse { ok: boolean; task: unknown }
|
|
389
388
|
export interface PostTasksIdVisualRequest { image_b64: string; content_type: string; alt?: string }
|
|
390
389
|
export interface PostTasksIdVisualResponse { ok: boolean; visual_url: unknown; visual_alt: unknown; bytes: unknown }
|
|
@@ -1203,7 +1202,7 @@ export interface BongosClient {
|
|
|
1203
1202
|
/** POST /tasks/{id}/promote — rank: metic+archon */
|
|
1204
1203
|
postTasksIdPromote(args?: RequestArgs): Promise<ApiResponse>;
|
|
1205
1204
|
/** POST /tasks/{id}/publish-branch — rank: any-builder */
|
|
1206
|
-
postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<
|
|
1205
|
+
postTasksIdPublishBranch(args: RequestArgs & { body: PostTasksIdPublishBranchRequest }): Promise<ApiResponse>;
|
|
1207
1206
|
/** GET /tasks/{id}/publish-status — rank: any-builder */
|
|
1208
1207
|
getTasksIdPublishStatus(args?: RequestArgs): Promise<ApiResponse>;
|
|
1209
1208
|
/** GET /tasks/{id}/recommendations — rank: any-builder */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ADR 0279 — An upgrade is proven by the served version, not by a health check
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-11
|
|
5
|
+
- **Task:** [task 1002884](https://cloudbongos.com/builders#/task/1002884) (BONGOS-V2, goal 1000090 — *Working area 4, Bongos Core distribution*), from idea 1000682
|
|
6
|
+
- **Extends** the mandatory post-restart health check of task 1002222 (audit H9, finding F5) and the auto-rollback of task 2149, whose unattended policy is [ADR 0136](<redacted>.md) §3. The health check stays; this says why it was never sufficient on its own.
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
On 2026-08-11 the auto-upgrade sweep on cloudbongos.com printed `✓ upgrade complete — core 1.19.13 → 1.19.56`, wrote a success row to the `core_upgrades` ledger and exited 0. Live kept serving **1.19.13**. Every monitoring surface agreed the upgrade had landed. None of them had looked at the running process.
|
|
11
|
+
|
|
12
|
+
Three things had to line up, and all three were in the shipped code:
|
|
13
|
+
|
|
14
|
+
1. **The restart failed, and that was a warning.** The sweep ran `systemctl restart cloudbongos.service` as user `lars` with no TTY. The unit is a `User=` unit, so the restart needed authorization polkit could only get interactively — `Interactive authentication required`, exit 1. `upgrade.js` logged `! restart command failed` and **fell through to verification**.
|
|
15
|
+
2. **The version check read the wrong thing.** `readInstalledCoreVersion()` reads `node_modules/@bongos/core/package.json` — the version on **disk**. `npm install` had genuinely put 1.19.56 there. Disk was right; the process was not.
|
|
16
|
+
3. **The health check could not tell the difference.** `pollHealth()` asks whether *something* answers with a 2xx. The old process was up and perfectly healthy, so it answered. A health check confirms a port is being served; it cannot confirm *what* is serving it.
|
|
17
|
+
|
|
18
|
+
Each check was individually reasonable. Together they were a closed loop that could not observe the failure they existed to catch, which is the worst shape a check can have: it reports success it has not proven, and a lying tool is more expensive than a broken one, because nothing goes looking.
|
|
19
|
+
|
|
20
|
+
The damage outlived the incident. `.claude/scheduled-tasks/core-update-subscription/subscribe.js` routes around `bongos upgrade` in favour of `go-live.js` and says so in a comment — *"upgrade.js's own health check is documented to false-pass by polling the still-running OLD process"*. A known-lying tool had become something the rest of the system worked around instead of fixing.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
**A bump is confirmed by asking the running process what version it is. Anything less is unconfirmed, and unconfirmed is not success.**
|
|
25
|
+
|
|
26
|
+
Three changes in `scripts/gds/upgrade.js`:
|
|
27
|
+
|
|
28
|
+
1. **A failed restart fails the upgrade.** It enters the same auto-rollback path as a failed install, migrate or health check, and returns non-zero. Rolling back rather than merely erroring keeps disk and process consistent: the previous core goes back on disk, matching the one that is (still) running. Leaving disk ahead of the process is the state that made the original incident invisible.
|
|
29
|
+
|
|
30
|
+
2. **The served version is read back from `/version`.** That route reports the live process's `coreVersion` (`serve-internal.js`, since 1.17.2) and is exempt from the prelaunch gate, so it is readable wherever `/healthz` is. The URL defaults to `/version` on the health URL's origin and can be overridden with `--version-url`. Deriving rather than requiring a new flag is deliberate: every existing call site passes only `--health-url`, and a check that must be opted into is off exactly where it is needed.
|
|
31
|
+
|
|
32
|
+
3. **A mismatch fails; an unreadable endpoint warns.** These are different facts and are treated differently. *"It says 1.19.13 and I asked for 1.19.56"* is proof of failure — roll back. *"It would not tell me"* is absence of proof: an instance may not expose the endpoint, and refusing every such bump would be a worse regression than the false-pass we are removing. Passing `--version-url` explicitly asks for proof, so there an unreadable endpoint **is** a failure — which is what the unattended subscription lane now does, since its roster already carries the exact URL.
|
|
33
|
+
|
|
34
|
+
**On escalation: `sudo -n`, not a hand-placed polkit rule.** `restartService()` retries a failed `systemctl restart` through `sudo -n` when not root — the idiom `dev.js` and `dev-lib.js` already use, and consistent with `provision.js`, which prefixes its own `systemctl` calls with `sudo`. `upgrade.js` was the one place that restarted a service without escalating, which is why only it hit the wall. The live mitigation at the time was a hand-written `/etc/polkit-1/rules.d/<redacted>.rules` on one box; that rule is **superseded**, because a rebuilt box inherits code and does not inherit hand-placed `/etc` files. `-n` fails fast rather than hanging on a password prompt, so the retry costs nothing where it isn't permitted. The polkit route is documented in [`docs/recipes/instance-service-restart.md`](../recipes/instance-service-restart.md) for hosts that prefer it or that deliberately withhold sudo.
|
|
35
|
+
|
|
36
|
+
## Consequences
|
|
37
|
+
|
|
38
|
+
- An upgrade that cannot restart the service now **fails loudly and reverts**, where it used to warn and claim success. This is the point, but it means a host with neither passwordless sudo nor a polkit rule will start seeing upgrades fail that previously "passed" — correctly. The error names both fixes.
|
|
39
|
+
- `--no-health-check` remains the single documented escape hatch and now waives the served-version read-back too, rather than adding a second flag to reason about.
|
|
40
|
+
- `subscribe.js` no longer needs to describe the direct-upgrade lane as having "no independent read-back"; the comment and the log line were corrected with the fix, since a stale comment about a fixed bug is how the next session re-learns a lie.
|
|
41
|
+
- The rollback path calls `restartService()` too, so it also escalates — a rollback on a box that needs sudo can now actually restart onto the restored core instead of silently leaving the failure in place.
|
|
42
|
+
|
|
43
|
+
## Rejected
|
|
44
|
+
|
|
45
|
+
- **Comparing `startedAt` instead of `coreVersion`.** A restart that swaps nothing still moves `startedAt` if the unit bounced, and a process that never restarted keeps an old one — but `startedAt` cannot distinguish "restarted onto the same (old) core" from "restarted onto the new one". `coreVersion` answers the actual question.
|
|
46
|
+
- **Making an unreadable endpoint fatal by default.** Fails closed in the wrong direction: it would break upgrades on instances that never served `/version`, to catch a case that a fatal restart failure already catches.
|
|
47
|
+
- **Requiring `--version-url` everywhere.** The check would then be absent from every current call site — the same "off where it matters" failure in a new costume.
|
|
48
|
+
- **Keeping the polkit rule as the answer.** It works, but it lives in `/etc` on one machine. The task's own framing is that a rebuilt box must inherit the fix; only code does that.
|
package/docs/adr/README.md
CHANGED
|
@@ -370,3 +370,4 @@ This keeps the decision history honest and traceable.
|
|
|
370
370
|
| 0276 | [**The skill-listing budget cannot hold 64 skills and every trigger, so the number is ratcheted and the choice goes to the owner** ([task 1003620](https://cloudbongos.com/builders#/task/1003620) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). Every SKILL.md description is resident in EVERY session for every craft, and `skill-lint` budgets the whole listing at 8,000 chars. It has been trimmed twice ([#1003548], [#1003587]) and grown back both times, because the 400-char per-description aim was a WARNING THAT ACCUMULATES. This task rewrote 49 of the 50 `.claude/skills/` descriptions — 17 warnings to 0, listing 25,624 → 19,316 chars, about 1,570 tokens returned to every session — with every trigger phrase preserved and CHECKED MECHANICALLY (a checker diffed the quoted phrases against HEAD and caught five real losses, all restored; four dropped strings were UI labels, not triggers). **8,000 was not reached, and the arithmetic says it cannot be:** across 64 skills the names (880) plus the mandatory quoted triggers (~5,370) are an irreducible ~6,250, leaving ~27 chars per skill to say what the skill DOES. Reaching the budget means trigger-only descriptions — trading truncation for the loss of the semantic signal routing leans on when the user’s words do not literally match a trigger. **Decision: ratchet `skill_listing_chars` at 19,316** in `fitness-ratchets.js` (fails open if the linter cannot load), so it cannot grow while the real question is open; the baseline is deliberately ABOVE skill-lint’s budget and is not a claim the budget is met. **Open and owner-gated:** delist skills (64 is a menu), scale the budget with the roster, or accept trigger-only text — recommendation is delist then rescale, filed as a blocker. `fitness.js`’s over-budget warning STAYS, as the visible trace of that question. Rejected: raising `LISTING_BUDGET_CHARS` to silence the warning (deletes the signal, not the debt); re-cutting the 14 module ui-design descriptions a week after [#1003587] wrote them, for ~2,000 chars toward a target still 9,000 away.](<redacted>.md) | skills / context budget / routing |
|
|
371
371
|
| 0277 | [**A box is "in use" only while a human is attached, and the claim expires** ([task 1003507](https://cloudbongos.com/builders#/task/1003507) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). `sweep-idle` skipped any box with `claude_active = true` at ANY age, and `claude_active` is a LATCH, not a level: only a heartbeat ping writes it, and `infra/box-heartbeat.sh` exits WITHOUT pinging when it sees nothing — so silence, the very signal the sweep exists to act on, could never clear it. Reproduced against the shipped selector: skipped at `idleMinutes` of 10, 120, 1440 and **5,256,000** (ten years). Not reaped late; never. The second leg is that `load > 0.2` kept `last_activity_at` bumping every 5 minutes anyway (a devcontainer plus an idle `claude` clears that floor on its own), so EITHER leg alone kept a box alive — which is why bounding only the veto looks like a fix and is not: the incident box's heartbeat was FRESH. Measured: `example-owner`'s box up since 2026-08-12, tmux `otb` UNATTACHED since 2026-08-15 18:01 UTC, `claude` burning 16 min of CPU across 25h of wall clock, **$20.21** of mostly-unattended compute. A THIRD defect, found while testing this and confirmed on clean `origin/main`, made the sweep inert regardless: `loadDeps()` read `_deps` before anything declared it, so it threw `ReferenceError` on first call and every command resolving a DigitalOcean client went with it — including `sweep-idle --apply`, which builds that client before the park loop. **The idle sweep could not park any box at all**, hidden because the suite's only `apply: true` test relied on the veto emptying the idle set before `makeDo()` was reached: one bug shielded by the other. **Decision: a box may not stay active longer than `BOX_UNATTENDED_MAX_HOURS` (12) without evidence a HUMAN was attached.** The heartbeat already computed that signal and folded it into one boolean; it now reports `attached` separately (login session, inbound SSH, open ttyd, or an ATTACHED tmux client via `#{session_attached}` — the signal that separates this box from a working one) and the server stamps `last_attached_at`. `claude_active` keeps its veto but it EXPIRES, bounded by a `claude_active_since` edge stamp. `pgrep -x claude` and the load floor remain reasons the box PINGS, never evidence anyone is THERE — a running process is not a person, and that distinction is the whole decision. The two `NULL` defaults deliberately DISAGREE: `last_attached_at` NULL means "no data" and keeps a pre-1003507 box on the old behaviour (core_238 pointedly does NOT backfill it — a backfilled `now()` starts a clock nothing can advance and parks every un-upgraded box one cap later, and box source sync is not prompt: idea 1000745 records 257 commits behind for three days), while `claude_active_since` NULL is REFUSED because an unknown latch age is the forever-latch itself, and is backfilled so the state is unreachable after deploy. 12h because parking is reversible since task 1002726 (snapshot kept), so a false positive costs one wake against $20.21 for no cap. Both clocks clear at park/wake/deprovision, or a woken box inherits an expired latch and is parked instantly — the fix reintroducing the bug from the far side. Rejected: tracking attachment INSTEAD of bounding the latch (the silent box keeps `true` forever, so the reported hole survives); bounding the latch alone (built first, and the verification probe caught it — the fresh heartbeat meant lifting the veto changed nothing); deleting the load floor (it is what makes an autonomous run count); measuring from `active_since` (that is uptime — parks a box worked on for days); raising `BOX_IDLE_MINUTES` (no threshold reaches an unbounded veto).](<redacted>.md) | dev box / cost / idle sweep |
|
|
372
372
|
| 0278 | [**A gated project still takes applications, and the exemption is scoped to the verb** ([task 1003525](https://cloudbongos.com/builders#/task/1003525) · the apply write itself in [task 1003624](https://cloudbongos.com/builders#/task/1003624) · goal 1000106 — *Working area 1, Project creation*; owner decision 2026-09-11). A project's owner sets who may SEE it (`platformVisibility`, [ADR 0192](<redacted>.md)) and who may JOIN it (`joinability`, [ADR 0194](<redacted>.md)) independently — and set to their middle values, members-only AND apply-to-join, the project took no applications at all: the member door refused every cookie-less request with `401` before the public `POST <api>/access-requests` could answer, because that write was not on the exempt list. The two settings composed into **"nobody can apply"**, which nobody chose. It survived because nothing LIED about it — the hub's join box relayed the project's own `401` honestly as `members_only`, and the hall's landing, where the apply form lives, is itself behind the door; the composition was simply unreachable. Found by the R14 proof ([task 1002333](https://cloudbongos.com/builders#/task/1002333)). ADR 0192 §3 had fixed the exempt list at "the door, the manifest, the probes and the downloads — and nothing wider" and left widening it as an owner call, which is what this is. **Decision: yes — and BOTH halves are exempted, each scoped to one path and one verb.** `POST <api>/access-requests` (it grants nothing — an application is a row in a queue the owner still reviews, [ADR 0201](<redacted>.md), already public on every non-gated project) and `GET <api>/access-requests/status` (without it the answer is half an answer: `bongos login` cannot re-poll the device flow after a `not_approved` — the `device_code` is spent — so an applicant would file a request and then wait on an approval they can never observe). `EXEMPT` entries may now be `{ re, methods }` beside the bare `RegExp`s, and `isExempt(path, method)` takes the verb as an OPTIONAL second argument that **fails closed** for a scoped entry when none is given, so the one-argument static callers cannot accidentally widen. **The verb is load-bearing, not tidiness:** the bare `GET` on `<api>/access-requests` is the OWNER'S QUEUE (`requireBuilder` + `access_request.review`), the surface listing would-be builders by name with their vouch state — a path-only exemption would have silently taken the member door off the front of it, leaving one layer where there were two, and the queue's own `requirePermission` still holding is exactly what makes that loss easy to miss. **It opens no oracle the gate was closing:** the status route's boolean twin `GET <api>/auth/web/admission-status` is ALREADY reachable on a gated project inside the `auth/*` subtree §3 exempts whole (§3 records that cost in as many words), and the two share ONE per-IP budget on purpose ([ADR 0209](<redacted>.md)) so neither can be alternated against the other. What it DOES add, stated as the honest cost: applicant detail — `pending`/`dismissed`/`none` over the twin's bare `admitted`. Whether that answer should collapse is ADR 0209's still-open owner question and is deliberately NOT decided here. No hub change: `joinRelayOutcome` maps the RELAYED status, so it carries the project's real answer the moment the `401` stops. Rejected: "gated means gated" — hide *Apply to join* and say so in the manage blurb (coherent, and the call went the other way); exempting the path without the verb; exempting the write alone; collapsing the status response while the route happened to be open (that is how a deferred decision gets made by accident).](<redacted>.md) | project visibility / join door / member door |
|
|
373
|
+
| 0279 | [**An upgrade is proven by the served version, not by a health check** ([task 1002884](https://cloudbongos.com/builders#/task/1002884) · goal 1000090 — *Working area 4, Bongos Core distribution*; from idea 1000682). On 2026-08-11 the auto-upgrade sweep printed `✓ upgrade complete — core 1.19.13 → 1.19.56`, wrote a success row to `core_upgrades` and exited 0 while live kept serving **1.19.13**. Three shipped checks formed a closed loop that could not see the failure they existed to catch: the `systemctl restart` failed with `Interactive authentication required` (a `User=` unit, no TTY) and `upgrade.js` treated it as a WARNING and fell through; `readInstalledCoreVersion()` then confirmed the version on **disk**, where `npm install` had correctly put it; and `pollHealth()` got a 200 from the **still-running old process**, because a health check confirms a port is served, never *what* serves it. A lying tool is worse than a broken one — nothing goes looking. The damage outlived the incident: `subscribe.js` had already routed the unattended lane around `bongos upgrade` in favour of `go-live.js`, citing this false-pass in a comment. **Decision: a bump is confirmed by asking the running process what version it is.** (1) A failed restart enters the same auto-rollback path as a failed install/migrate/health and exits non-zero — rolling back rather than merely erroring keeps disk and process consistent, since disk-ahead-of-process is the state that made the incident invisible. (2) The served version is read back from `/version` (`coreVersion`, since 1.17.2, prelaunch-gate exempt), defaulting to the `--health-url` origin and overridable via `--version-url` — derived rather than opt-in because every existing call site passes only `--health-url`, and a check you must opt into is off exactly where it is needed. (3) A **mismatch** fails (proof of failure → roll back); an **unreadable** endpoint only warns (absence of proof — refusing every such bump would regress harder than the false-pass), except under an explicit `--version-url`, which asks for proof and therefore gets a failure. That strict mode is what the unattended subscription lane now passes, its roster already carrying the URL. **Escalation is `sudo -n`, not a hand-placed polkit rule:** `restartService()` retries a failed restart through `sudo -n` when not root — the idiom `dev.js`/`dev-lib.js` already use and consistent with `provision.js`, `upgrade.js` having been the one place that restarted without escalating. The live `/etc/polkit-1/rules.d/<redacted>.rules` mitigation is superseded: a rebuilt box inherits code, not hand-placed `/etc` files (polkit route kept in [`docs/recipes/instance-service-restart.md`](../recipes/instance-service-restart.md)). `--no-health-check` stays the single escape hatch and now waives the read-back too. Rejected: comparing `startedAt` (cannot distinguish a restart onto the same old core from one onto the new); making an unreadable endpoint fatal by default; requiring `--version-url` everywhere (absent from every current call site — the same "off where it matters" failure in a new costume); keeping polkit as the answer.](<redacted>.md) | core distribution / upgrade verification |
|
package/docs/api/openapi.json
CHANGED
|
@@ -15704,14 +15704,7 @@
|
|
|
15704
15704
|
},
|
|
15705
15705
|
"responses": {
|
|
15706
15706
|
"200": {
|
|
15707
|
-
"description": "Success."
|
|
15708
|
-
"content": {
|
|
15709
|
-
"application/json": {
|
|
15710
|
-
"schema": {
|
|
15711
|
-
"$ref": "#/components/schemas/PostTasksIdPublishBranchResponse"
|
|
15712
|
-
}
|
|
15713
|
-
}
|
|
15714
|
-
}
|
|
15707
|
+
"description": "Success."
|
|
15715
15708
|
},
|
|
15716
15709
|
"400": {
|
|
15717
15710
|
"$ref": "#/components/responses/ValidationFailed"
|
|
@@ -22442,25 +22435,6 @@
|
|
|
22442
22435
|
],
|
|
22443
22436
|
"additionalProperties": false
|
|
22444
22437
|
},
|
|
22445
|
-
"PostTasksIdPublishBranchResponse": {
|
|
22446
|
-
"type": "object",
|
|
22447
|
-
"properties": {
|
|
22448
|
-
"ok": {
|
|
22449
|
-
"type": "boolean"
|
|
22450
|
-
},
|
|
22451
|
-
"pr_url": {},
|
|
22452
|
-
"pr_number": {},
|
|
22453
|
-
"head_sha": {},
|
|
22454
|
-
"auto_merge": {}
|
|
22455
|
-
},
|
|
22456
|
-
"required": [
|
|
22457
|
-
"ok",
|
|
22458
|
-
"pr_url",
|
|
22459
|
-
"pr_number",
|
|
22460
|
-
"head_sha",
|
|
22461
|
-
"auto_merge"
|
|
22462
|
-
]
|
|
22463
|
-
},
|
|
22464
22438
|
"PostTasksIdShipResponse": {
|
|
22465
22439
|
"type": "object",
|
|
22466
22440
|
"properties": {
|
|
@@ -23019,8 +22993,8 @@
|
|
|
23019
22993
|
}
|
|
23020
22994
|
},
|
|
23021
22995
|
"x-endpoint-count": 363,
|
|
23022
|
-
"x-schema-count":
|
|
22996
|
+
"x-schema-count": 401,
|
|
23023
22997
|
"x-undocumented-bodies": 9,
|
|
23024
|
-
"x-response-schemas":
|
|
22998
|
+
"x-response-schemas": 270,
|
|
23025
22999
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
23026
23000
|
}
|
|
@@ -1817,5 +1817,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1817
1817
|
landed since 1.19.677 with no explicit bump. run 34653458123. (task 1002620)
|
|
1818
1818
|
1.19.679 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1819
1819
|
landed since 1.19.678 with no explicit bump. run 34657605820. (task 1002620)
|
|
1820
|
+
1.19.680 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1821
|
+
landed since 1.19.679 with no explicit bump. run 34665216511. (task 1002620)
|
|
1822
|
+
1.19.681 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1823
|
+
landed since 1.19.680 with no explicit bump. run 34666648354. (task 1002620)
|
|
1820
1824
|
---------------------------------------------------------------------------
|
|
1821
1825
|
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Recipe — letting `bongos upgrade` restart the instance service unattended
|
|
2
|
+
|
|
3
|
+
> [ADR 0279](../adr/<redacted>.md) · [task 1002884](https://cloudbongos.com/builders#/task/1002884). A core bump only counts once the **running process** is the new core, so `bongos upgrade` has to be able to restart the service without a human at a keyboard. This is what to do when it cannot.
|
|
4
|
+
|
|
5
|
+
## The symptom
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
! restart command failed (exit 1) — restart the service manually.
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or, in the journal for the unit:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Interactive authentication required.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Since task 1002884 this **fails the bump and rolls it back** rather than warning and carrying on. That is the fix working: before it, the run went on to health-check the still-running old process, got a 200, and reported `✓ upgrade complete` for a core that was never served.
|
|
18
|
+
|
|
19
|
+
## Why it happens
|
|
20
|
+
|
|
21
|
+
A provisioned instance runs under a `User=` systemd unit. `systemctl restart <unit>` from the owning (non-root) user is a privileged action, so systemd asks polkit, and polkit wants to authenticate a human. In an interactive shell you get a password prompt. In the unattended sweep there is no TTY, so polkit refuses outright and `systemctl` exits 1.
|
|
22
|
+
|
|
23
|
+
## The fix, in order of preference
|
|
24
|
+
|
|
25
|
+
### 1. Passwordless sudo (what the code already tries)
|
|
26
|
+
|
|
27
|
+
`upgrade.js` retries a failed `systemctl restart` through `sudo -n systemctl restart <unit>` when it is not running as root — the same escalation `dev.js`, `dev-lib.js` and `provision.js` use. If the service owner has passwordless sudo, **nothing needs configuring** and the bump succeeds on the retry (the log says `✓ restarted <unit> (via sudo -n)`).
|
|
28
|
+
|
|
29
|
+
Check whether the owner already has it:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sudo -n -l
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A `sudo: a password is required` means no. Note the `-n`: without it this command blocks on a prompt, which is exactly the failure being diagnosed.
|
|
36
|
+
|
|
37
|
+
### 2. Scoped sudoers, if blanket sudo is too much
|
|
38
|
+
|
|
39
|
+
Grant only the restart, not everything. Write it with `visudo -f` so a syntax error cannot lock the host out:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sudo visudo -f /etc/sudoers.d/50-<slug>-restart
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
<owner> ALL=(root) NOPASSWD: <redacted> restart <slug>.service
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then confirm the retry path works, non-interactively:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
sudo -n systemctl restart <slug>.service && echo OK
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. A polkit rule, for hosts that deliberately withhold sudo
|
|
56
|
+
|
|
57
|
+
Equivalent outcome, different mechanism — use it when policy forbids a sudoers entry.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
sudo tee /etc/polkit-1/rules.d/50-<slug>-restart.rules >/dev/null <<'RULE'
|
|
61
|
+
polkit.addRule(function(action, subject) {
|
|
62
|
+
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
|
63
|
+
action.lookup("unit") == "<slug>.service" &&
|
|
64
|
+
subject.user == "<owner>") {
|
|
65
|
+
return polkit.Result.YES;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
RULE
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
No daemon reload is needed; polkit picks up rules files on the next check.
|
|
72
|
+
|
|
73
|
+
> **This file is not inherited by a rebuilt box.** It lives in `/etc` on one machine. A reprovision restores code, not hand-placed host config — which is precisely why the `sudo -n` retry lives in the core and this page is the fallback rather than the answer. If you reach for this route, record it in the instance's own provisioning notes or the box will re-discover the stall.
|
|
74
|
+
|
|
75
|
+
## Verifying it actually worked
|
|
76
|
+
|
|
77
|
+
Do not trust the exit code alone — that is the failure mode this whole page exists because of. Ask the running process what it is:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
curl -s https://<instance>/version
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`coreVersion` in that response is the live process's core. If it still reads the old version after a "successful" restart, the process did not swap, and `bongos upgrade` will now catch that itself and roll back.
|
|
84
|
+
|
|
85
|
+
## Related
|
|
86
|
+
|
|
87
|
+
- [ADR 0279](../adr/<redacted>.md) — why a health check cannot confirm an upgrade
|
|
88
|
+
- [ADR 0136](../adr/<redacted>.md) §3 — the unattended subscription lane's health-gate + auto-rollback policy
|
|
89
|
+
- [`docs/recipes/ops-gotchas.md`](ops-gotchas.md) — the wider set of deploy traps
|
|
@@ -513,8 +513,21 @@ async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
|
|
|
513
513
|
// without auto-merge enabled, or a PR already mergeable, returns false and the
|
|
514
514
|
// caller surfaces the open PR (exactly like ciLand on a failed enable).
|
|
515
515
|
async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
516
|
+
return (await enableAutoMergeDetailed({ nodeId }, deps)).ok;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// enableAutoMergeDetailed — the same arming call, but it says WHY it failed
|
|
520
|
+
// (task 1001431). enableAutoMerge collapses every failure to `false`, so the route
|
|
521
|
+
// answered a builder with `auto_merge: false` and no cause, and the single most
|
|
522
|
+
// common cause is not an outage at all: a repository with `allow_auto_merge` OFF
|
|
523
|
+
// can never be armed, so every publish reports the same silent false forever and
|
|
524
|
+
// the PR waits for a human nobody told. Returns { ok, reason } — reason is null on
|
|
525
|
+
// success, else a short scrubbed string safe to hand the (ownership-gated) builder.
|
|
526
|
+
// enableAutoMerge stays the boolean façade so existing callers are untouched.
|
|
527
|
+
async function enableAutoMergeDetailed({ nodeId }, deps = {}) {
|
|
516
528
|
const token = await resolveToken(deps);
|
|
517
|
-
if (!token
|
|
529
|
+
if (!token) return { ok: false, reason: 'no GitHub token could be resolved' };
|
|
530
|
+
if (!nodeId) return { ok: false, reason: 'the pull request has no node id to arm' };
|
|
518
531
|
const doFetch = deps.fetchImpl || fetch;
|
|
519
532
|
const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:MERGE}){clientMutationId}}`;
|
|
520
533
|
try {
|
|
@@ -523,11 +536,15 @@ async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
|
523
536
|
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
524
537
|
body: JSON.stringify({ query, variables: { id: nodeId } }),
|
|
525
538
|
});
|
|
526
|
-
if (!res.ok) return false;
|
|
539
|
+
if (!res.ok) return { ok: false, reason: `GitHub GraphQL returned HTTP ${res.status}` };
|
|
527
540
|
const out = await res.json();
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
541
|
+
if (!out || !out.errors) return { ok: true, reason: null };
|
|
542
|
+
// GitHub answers a repo with auto-merge disabled with a plain GraphQL error;
|
|
543
|
+
// its own message is the actionable one, so pass it through (truncated).
|
|
544
|
+
const msg = out.errors.map((e) => e && e.message).filter(Boolean).join('; ');
|
|
545
|
+
return { ok: false, reason: scrubCredential(msg || 'GitHub GraphQL rejected the mutation').slice(0, 300) };
|
|
546
|
+
} catch (e) {
|
|
547
|
+
return { ok: false, reason: scrubCredential((e && e.message) || 'the GraphQL request threw').slice(0, 300) };
|
|
531
548
|
}
|
|
532
549
|
}
|
|
533
550
|
|
|
@@ -1388,6 +1405,7 @@ module.exports = {
|
|
|
1388
1405
|
pushBundle,
|
|
1389
1406
|
openOrFindPullRequest,
|
|
1390
1407
|
enableAutoMerge,
|
|
1408
|
+
enableAutoMergeDetailed,
|
|
1391
1409
|
getPublishStatus,
|
|
1392
1410
|
getMergeState,
|
|
1393
1411
|
mergeIfGreen,
|
|
@@ -67,6 +67,33 @@ const { MIGRATION_KEYWORD_RE, VALID_FITS, badKind, deHashRefs, detectsMigration,
|
|
|
67
67
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
68
68
|
const log = api.logger('lifecycle');
|
|
69
69
|
const registerTaskWriteRoutes = require('./task-write-routes.js'); // task 1003206 — POST /tasks + PATCH /tasks/:id
|
|
70
|
+
// The body of the generic `publish_failed` 500 from POST /tasks/:id/publish-branch.
|
|
71
|
+
// Pure and exported so the response a builder actually receives is testable without
|
|
72
|
+
// standing up the route (the file's existing convention — see rankAttestsGateTrust).
|
|
73
|
+
//
|
|
74
|
+
// task 1001431 added `code` + `detail`. Seven error codes get their own mapped response
|
|
75
|
+
// above the fallthrough; every OTHER code github-push can throw — APP_TOKEN_FAILED,
|
|
76
|
+
// GRAPHQL_FAILED, BAD_PR, PR_PATCH_FAILED, STATUS_FETCH_FAILED — arrived here and was
|
|
77
|
+
// dropped on the floor, so the builder saw a bare `publish_failed` with nothing to act
|
|
78
|
+
// on and no way to tell an expired App token from a GraphQL outage. The code is an
|
|
79
|
+
// internal constant rather than user data, and this route is ownership-gated (claim
|
|
80
|
+
// holder or Archon) and already returns scrubbed git stderr — a short code name is
|
|
81
|
+
// strictly less revealing than what it ships beside.
|
|
82
|
+
//
|
|
83
|
+
// Every field stays OPTIONAL and keeps its existing name and meaning: ship.js prints
|
|
84
|
+
// `reason`, and older readers must not start seeing a key change shape under them.
|
|
85
|
+
function publishFailedDetail(e = {}) {
|
|
86
|
+
return {
|
|
87
|
+
...(e.code ? { code: e.code } : {}),
|
|
88
|
+
...(e.detail ? { detail: e.detail } : {}),
|
|
89
|
+
...(e.git_step ? { step: e.git_step } : {}),
|
|
90
|
+
...(e.stderr ? { reason: e.stderr } : {}),
|
|
91
|
+
...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
|
|
92
|
+
...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
|
|
93
|
+
...(e.timed_out ? { timed_out: true } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
module.exports = function buildTasksRouter() {
|
|
71
98
|
const router = express.Router();
|
|
72
99
|
|
|
@@ -1076,7 +1103,11 @@ module.exports = function buildTasksRouter() {
|
|
|
1076
1103
|
`Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
|
|
1077
1104
|
`Auto-merge is enabled — GitHub lands it once the required checks pass.`;
|
|
1078
1105
|
const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
|
|
1079
|
-
|
|
1106
|
+
// task 1001431: ask WHY when arming fails. `auto_merge: false` on its own told the
|
|
1107
|
+
// builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
|
|
1108
|
+
// a permanent condition that reads exactly like a transient blip.
|
|
1109
|
+
const autoMergeResult = await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
|
|
1110
|
+
const autoMerge = autoMergeResult.ok;
|
|
1080
1111
|
// task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
|
|
1081
1112
|
// gate-review check can auto-approve a Metic+ author's gate-surface PR with no
|
|
1082
1113
|
// owner click. Only the SERVER can set this (it holds req.builder's server-
|
|
@@ -1098,6 +1129,8 @@ module.exports = function buildTasksRouter() {
|
|
|
1098
1129
|
}
|
|
1099
1130
|
return res.status(201).json({
|
|
1100
1131
|
ok: true, pr_url: pr.url, pr_number: pr.number, head_sha: pushedSha, auto_merge: autoMerge,
|
|
1132
|
+
// Only present when arming failed — the publish itself still succeeded (201).
|
|
1133
|
+
...(autoMerge ? {} : { auto_merge_reason: autoMergeResult.reason }),
|
|
1101
1134
|
});
|
|
1102
1135
|
} catch (e) {
|
|
1103
1136
|
if (e.code === 'TIP_MISMATCH') {
|
|
@@ -1134,13 +1167,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1134
1167
|
// `reason` keeps its name and position so existing readers (ship.js prints it)
|
|
1135
1168
|
// keep working; it is simply no longer amputated. Everything here is already
|
|
1136
1169
|
// credential-scrubbed at the throw site by github-push.scrubCredential.
|
|
1137
|
-
return res.fail('publish_failed', 500,
|
|
1138
|
-
...(e.git_step ? { step: e.git_step } : {}),
|
|
1139
|
-
...(e.stderr ? { reason: e.stderr } : {}),
|
|
1140
|
-
...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
|
|
1141
|
-
...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
|
|
1142
|
-
...(e.timed_out ? { timed_out: true } : {}),
|
|
1143
|
-
});
|
|
1170
|
+
return res.fail('publish_failed', 500, publishFailedDetail(e));
|
|
1144
1171
|
} finally {
|
|
1145
1172
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
1146
1173
|
}
|
|
@@ -1336,6 +1363,7 @@ module.exports.MIGRATION_KEYWORD_RE = MIGRATION_KEYWORD_RE;
|
|
|
1336
1363
|
// #871: exported so tests/task_ownership_gate.mjs can drive the claim-ownership
|
|
1337
1364
|
// gate directly (with a stubbed db.getLastClaimHolder) — no server/DB needed.
|
|
1338
1365
|
module.exports.gateTaskOwnership = gateTaskOwnership;
|
|
1366
|
+
module.exports.publishFailedDetail = publishFailedDetail;
|
|
1339
1367
|
// task 1053 (F): exported so the verify-attestation guard is unit-tested.
|
|
1340
1368
|
module.exports.isVerifyTask = isVerifyTask;
|
|
1341
1369
|
// task 1139 / 1169 (ADR 0058): exported so the pure rank-attestation decision
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.681",
|
|
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.681",
|
|
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.681",
|
|
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",
|