@bongos/core 1.19.839 → 1.19.840
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 +52 -27
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md +49 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +2 -0
- package/docs/architecture.md +2 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/github-push.js +2 -2
- package/modules/lifecycle/pr-assign.js +77 -0
- package/modules/lifecycle/routes/tasks.js +16 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/paste-token.js +65 -14
- package/scripts/gds/provision-config.js +3 -0
- package/scripts/gds/provision-net.js +32 -0
- package/scripts/gds/provision.js +21 -4
- package/scripts/gds/ship-land.js +12 -0
- package/src/bongos/routes/auth.js +24 -3
- package/src/module-api.js +1 -1
- package/tests/bongos_cli.mjs +13 -4
- package/tests/cli_token_reissue.mjs +54 -7
- package/tests/paste_token_api_base.mjs +87 -0
- package/tests/pr_assign.mjs +189 -0
- package/tests/provision.mjs +12 -0
- package/tests/provision_caddy_wiring.mjs +148 -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.840",
|
|
6
|
+
"core_contract": "1.19.840",
|
|
7
|
+
"source_commit": "47fbba53d86033101f57cf1e1a85aef7367fe320",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T17:
|
|
9
|
+
"built_at": "2026-09-17T17:45:41.665Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 495,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2262,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2783,
|
|
20
|
+
"tree_sha256": "5ddd99652c37c0ad56f34d618732a671733aed9b055c1c4ee161135581d0c80a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "031abfbccd58bf9d6cdf9368f0d3bc862ddcd2f373b585a6911196b28596fae2"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1999,10 +1999,15 @@
|
|
|
1999
1999
|
"mode": "0000644",
|
|
2000
2000
|
"sha256": "08ea781a488d7454ca49b17e9540bb4c7bb9f1925aedf8ad4634057a89dd0d22"
|
|
2001
2001
|
},
|
|
2002
|
+
{
|
|
2003
|
+
"path": "docs/adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md",
|
|
2004
|
+
"mode": "0000644",
|
|
2005
|
+
"sha256": "62ffb9c1aeeaaafd5c51767f1ffff1c67adb3563e8d55ceb81ef77511184faa4"
|
|
2006
|
+
},
|
|
2002
2007
|
{
|
|
2003
2008
|
"path": "docs/adr/README.md",
|
|
2004
2009
|
"mode": "0000644",
|
|
2005
|
-
"sha256": "
|
|
2010
|
+
"sha256": "48323e44fced698a0da997eeb16564ab3b04430c8233a8f7997c0fec2b123401"
|
|
2006
2011
|
},
|
|
2007
2012
|
{
|
|
2008
2013
|
"path": "docs/api-reference.md",
|
|
@@ -2012,12 +2017,12 @@
|
|
|
2012
2017
|
{
|
|
2013
2018
|
"path": "docs/api/openapi.json",
|
|
2014
2019
|
"mode": "0000644",
|
|
2015
|
-
"sha256": "
|
|
2020
|
+
"sha256": "a0107dc012a5355baacf3668c92b17675b35ce0cffc96052b1d92d99533c5744"
|
|
2016
2021
|
},
|
|
2017
2022
|
{
|
|
2018
2023
|
"path": "docs/architecture.md",
|
|
2019
2024
|
"mode": "0000644",
|
|
2020
|
-
"sha256": "
|
|
2025
|
+
"sha256": "029e8c5a9562171251df353e08791f1520c2e17fa7d286c7b4d278dea195c6d7"
|
|
2021
2026
|
},
|
|
2022
2027
|
{
|
|
2023
2028
|
"path": "docs/branding-contract.md",
|
|
@@ -2567,7 +2572,7 @@
|
|
|
2567
2572
|
{
|
|
2568
2573
|
"path": "docs/module-api-changelog.md",
|
|
2569
2574
|
"mode": "0000644",
|
|
2570
|
-
"sha256": "
|
|
2575
|
+
"sha256": "f8fa8c024b4d6741ea672a7be33957d6cc58c6b85867ef2b5499bcc31fdf4b50"
|
|
2571
2576
|
},
|
|
2572
2577
|
{
|
|
2573
2578
|
"path": "docs/modules-contract.md",
|
|
@@ -5682,7 +5687,7 @@
|
|
|
5682
5687
|
{
|
|
5683
5688
|
"path": "modules/lifecycle/github-push.js",
|
|
5684
5689
|
"mode": "0000644",
|
|
5685
|
-
"sha256": "
|
|
5690
|
+
"sha256": "84114ecaf06c6ac52b7d7e4b5eaff97086908d6f7df0a3e07e4be3aa923935bc"
|
|
5686
5691
|
},
|
|
5687
5692
|
{
|
|
5688
5693
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -5804,6 +5809,11 @@
|
|
|
5804
5809
|
"mode": "0000644",
|
|
5805
5810
|
"sha256": "81ed257d2156988e911d737e10cba2b8375f47fc6e27bf77c06d9dc18258380b"
|
|
5806
5811
|
},
|
|
5812
|
+
{
|
|
5813
|
+
"path": "modules/lifecycle/pr-assign.js",
|
|
5814
|
+
"mode": "0000644",
|
|
5815
|
+
"sha256": "08ace9d2690ef7f563455bdd52f89e8f60587eb399a246726cdb6ae06e98699f"
|
|
5816
|
+
},
|
|
5807
5817
|
{
|
|
5808
5818
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
5809
5819
|
"mode": "0000644",
|
|
@@ -5912,7 +5922,7 @@
|
|
|
5912
5922
|
{
|
|
5913
5923
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
5914
5924
|
"mode": "0000644",
|
|
5915
|
-
"sha256": "
|
|
5925
|
+
"sha256": "760c76c6ce7ba52991991e7c5fd00a48466d4ad8ae8e61620d962cd4496ea06c"
|
|
5916
5926
|
},
|
|
5917
5927
|
{
|
|
5918
5928
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -7867,12 +7877,12 @@
|
|
|
7867
7877
|
{
|
|
7868
7878
|
"path": "package-lock.json",
|
|
7869
7879
|
"mode": "0000644",
|
|
7870
|
-
"sha256": "
|
|
7880
|
+
"sha256": "d54803c0123c3ee0b857092be850581f787ed85dc36cae70e737977f14f0356b"
|
|
7871
7881
|
},
|
|
7872
7882
|
{
|
|
7873
7883
|
"path": "package.json",
|
|
7874
7884
|
"mode": "0000644",
|
|
7875
|
-
"sha256": "
|
|
7885
|
+
"sha256": "0a21605b0152921f84f2e67716df7e251b09a23b24c3b9569d3332d8d98e754c"
|
|
7876
7886
|
},
|
|
7877
7887
|
{
|
|
7878
7888
|
"path": "public-docs/index.html",
|
|
@@ -8727,7 +8737,7 @@
|
|
|
8727
8737
|
{
|
|
8728
8738
|
"path": "scripts/gds/paste-token.js",
|
|
8729
8739
|
"mode": "0000644",
|
|
8730
|
-
"sha256": "
|
|
8740
|
+
"sha256": "cfc9e6a9640f8392a38077a7fa50bd1caae671a60c4735d2758c0cc5a6d10610"
|
|
8731
8741
|
},
|
|
8732
8742
|
{
|
|
8733
8743
|
"path": "scripts/gds/plain-cards.js",
|
|
@@ -8767,7 +8777,7 @@
|
|
|
8767
8777
|
{
|
|
8768
8778
|
"path": "scripts/gds/provision-config.js",
|
|
8769
8779
|
"mode": "0000644",
|
|
8770
|
-
"sha256": "
|
|
8780
|
+
"sha256": "b5ec038d150da93b4c54eeb864586a70e46610655ccb5bf10bd1adaa0432a305"
|
|
8771
8781
|
},
|
|
8772
8782
|
{
|
|
8773
8783
|
"path": "scripts/gds/provision-core-upgrade.js",
|
|
@@ -8782,7 +8792,7 @@
|
|
|
8782
8792
|
{
|
|
8783
8793
|
"path": "scripts/gds/provision-net.js",
|
|
8784
8794
|
"mode": "0000644",
|
|
8785
|
-
"sha256": "
|
|
8795
|
+
"sha256": "f1ca176ff8cdb8de65940af1e30316db600cb5df529abbc997697abf1cbc2c2d"
|
|
8786
8796
|
},
|
|
8787
8797
|
{
|
|
8788
8798
|
"path": "scripts/gds/provision-repo-auth.js",
|
|
@@ -8807,7 +8817,7 @@
|
|
|
8807
8817
|
{
|
|
8808
8818
|
"path": "scripts/gds/provision.js",
|
|
8809
8819
|
"mode": "0000644",
|
|
8810
|
-
"sha256": "
|
|
8820
|
+
"sha256": "43dd54ec69ff29ad83e631d53a65c1a2d22b89ef07181e4df14581e5199bfa61"
|
|
8811
8821
|
},
|
|
8812
8822
|
{
|
|
8813
8823
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -9182,7 +9192,7 @@
|
|
|
9182
9192
|
{
|
|
9183
9193
|
"path": "scripts/gds/ship-land.js",
|
|
9184
9194
|
"mode": "0000644",
|
|
9185
|
-
"sha256": "
|
|
9195
|
+
"sha256": "805017578a7d69ca2c79d105e21add7862b41e352256e5fbdfad3e3740e69ced"
|
|
9186
9196
|
},
|
|
9187
9197
|
{
|
|
9188
9198
|
"path": "scripts/gds/ship-memory-sync.js",
|
|
@@ -9692,7 +9702,7 @@
|
|
|
9692
9702
|
{
|
|
9693
9703
|
"path": "src/bongos/routes/auth.js",
|
|
9694
9704
|
"mode": "0000644",
|
|
9695
|
-
"sha256": "
|
|
9705
|
+
"sha256": "c29f47f08ea45126a8ddd6fd432bdf5c17a98cac0ad6f6ae527bf23e62ffa001"
|
|
9696
9706
|
},
|
|
9697
9707
|
{
|
|
9698
9708
|
"path": "src/bongos/routes/backup.js",
|
|
@@ -9802,7 +9812,7 @@
|
|
|
9802
9812
|
{
|
|
9803
9813
|
"path": "src/module-api.js",
|
|
9804
9814
|
"mode": "0000644",
|
|
9805
|
-
"sha256": "
|
|
9815
|
+
"sha256": "8bfa67a85b56ce33dbb502537edced8bf39de30551c7949bf3aa9460a1deffa3"
|
|
9806
9816
|
},
|
|
9807
9817
|
{
|
|
9808
9818
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10277,7 +10287,7 @@
|
|
|
10277
10287
|
{
|
|
10278
10288
|
"path": "tests/bongos_cli.mjs",
|
|
10279
10289
|
"mode": "0000644",
|
|
10280
|
-
"sha256": "
|
|
10290
|
+
"sha256": "4e845333aa949256b8b5c6dc73bf75fcc77047543b0f5e15e89172cad4161772"
|
|
10281
10291
|
},
|
|
10282
10292
|
{
|
|
10283
10293
|
"path": "tests/bongos_dispatch.mjs",
|
|
@@ -10562,7 +10572,7 @@
|
|
|
10562
10572
|
{
|
|
10563
10573
|
"path": "tests/cli_token_reissue.mjs",
|
|
10564
10574
|
"mode": "0000644",
|
|
10565
|
-
"sha256": "
|
|
10575
|
+
"sha256": "da6b45619e6848c37ebc8d53ffc9d8f9d5379df26563c9030b339a8c389e645d"
|
|
10566
10576
|
},
|
|
10567
10577
|
{
|
|
10568
10578
|
"path": "tests/client_baseurl_guard.mjs",
|
|
@@ -12374,6 +12384,11 @@
|
|
|
12374
12384
|
"mode": "0000644",
|
|
12375
12385
|
"sha256": "fd69c44ebf3d246ecedf527275448389cca809e231d1c6cb34e4c12bfdc38111"
|
|
12376
12386
|
},
|
|
12387
|
+
{
|
|
12388
|
+
"path": "tests/paste_token_api_base.mjs",
|
|
12389
|
+
"mode": "0000644",
|
|
12390
|
+
"sha256": "e48806b48a9f7a4f9f9c010f45750e6b440d2de68feacdabacd9bd25d98f2046"
|
|
12391
|
+
},
|
|
12377
12392
|
{
|
|
12378
12393
|
"path": "tests/patch_gate_ordering.mjs",
|
|
12379
12394
|
"mode": "0000644",
|
|
@@ -12434,6 +12449,11 @@
|
|
|
12434
12449
|
"mode": "0000644",
|
|
12435
12450
|
"sha256": "08a7496090f4617cef7e270afddcef8ee3a0b58bff6920b56bc2f2bbb49ed4ca"
|
|
12436
12451
|
},
|
|
12452
|
+
{
|
|
12453
|
+
"path": "tests/pr_assign.mjs",
|
|
12454
|
+
"mode": "0000644",
|
|
12455
|
+
"sha256": "1f422614784bff05cfddff5196fabfb9458e8b1247e6e6ecb636f1e4f236fc78"
|
|
12456
|
+
},
|
|
12437
12457
|
{
|
|
12438
12458
|
"path": "tests/pre_compact.mjs",
|
|
12439
12459
|
"mode": "0000644",
|
|
@@ -12627,7 +12647,12 @@
|
|
|
12627
12647
|
{
|
|
12628
12648
|
"path": "tests/provision.mjs",
|
|
12629
12649
|
"mode": "0000644",
|
|
12630
|
-
"sha256": "
|
|
12650
|
+
"sha256": "5a1abd44776798ca89266f86663037086c1270aca569e54647607d4488cbfea9"
|
|
12651
|
+
},
|
|
12652
|
+
{
|
|
12653
|
+
"path": "tests/provision_caddy_wiring.mjs",
|
|
12654
|
+
"mode": "0000644",
|
|
12655
|
+
"sha256": "a4d448622c05e963602cbc0488215ba168c909bd12cd0db20eaa6fcb7de7a91e"
|
|
12631
12656
|
},
|
|
12632
12657
|
{
|
|
12633
12658
|
"path": "tests/provision_disconnect.mjs",
|
|
@@ -216,7 +216,7 @@ export interface PostAuthAppPairStartRequest { device_name?: string; platform?:
|
|
|
216
216
|
export interface PostAuthAppPairStartResponse { pair_code: unknown; poll_token: unknown; verify_path: unknown; verify_url: unknown; expires_in: unknown; interval: unknown }
|
|
217
217
|
export interface PostAuthBackchannelLogoutRequest { logout_token: string }
|
|
218
218
|
export interface PostAuthBackchannelLogoutResponse { ok: boolean }
|
|
219
|
-
export interface PostAuthCliTokenIssueResponse { ok: boolean; token: unknown; expires_at: unknown; builder_login: unknown; paste_command: unknown }
|
|
219
|
+
export interface PostAuthCliTokenIssueResponse { ok: boolean; token: unknown; expires_at: unknown; builder_login: unknown; api_base: unknown; paste_command: unknown }
|
|
220
220
|
export interface PostAuthDeviceExchangeRequest { assertion: string }
|
|
221
221
|
export interface PostAuthDeviceExchangeResponse { token: unknown; builder: unknown }
|
|
222
222
|
export interface PostAuthDevicePollRequest { device_code: string }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ADR 0303 — Provisioning verifies the base Caddyfile, and never edits it
|
|
2
|
+
|
|
3
|
+
**Status:** accepted · **Date:** 2026-09-17 · **Task:** [task 1002860](https://cloudbongos.com/builders#/task/1002860) (goal 1000106 — project hosting & fleet; from idea 1000524)
|
|
4
|
+
|
|
5
|
+
**Related:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §4 (on-demand ACME behind the tls-check ask) · [ADR 0128](0128-provisioning-runner-privilege-and-db-auth-model.md) (the runner writes snippets under `sudo install`; it found the live Caddyfile missing the ask)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The provisioning runner writes one `<slug>.caddy` site snippet per instance and reloads Caddy. It
|
|
10
|
+
depends on two lines in the box's base `/etc/caddy/Caddyfile` that it never checked:
|
|
11
|
+
`on_demand_tls { ask …/provisioning/tls-check }` and `import /etc/caddy/sites/*.caddy`. Both were
|
|
12
|
+
hand-applied on the live boxes. The reload ran with `allowFail: true`. So on a box missing
|
|
13
|
+
either line, provisioning reported success and the instance was simply unreachable, with no
|
|
14
|
+
signal to anyone. The base Caddyfile is not in this repository. It is host configuration, and
|
|
15
|
+
it also serves the platform's own apex.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
1. **Detect and refuse, never edit.** The runner reads the base Caddyfile and does not write it.
|
|
20
|
+
A missing directive stops the run with the exact line to add and the file to add it to.
|
|
21
|
+
An automatic edit was rejected for two reasons. `on_demand_tls` has to sit inside the
|
|
22
|
+
global options block, which an append cannot place and a rewrite could corrupt. And a bad
|
|
23
|
+
write to that file takes down every site on the box, the control plane's own apex included.
|
|
24
|
+
The directives are a one-time setup per box, so the refusal only ever fires on a box that
|
|
25
|
+
was never configured.
|
|
26
|
+
2. **Read Caddy's resolved config, not the file's text.** The check runs
|
|
27
|
+
`caddy adapt --config <Caddyfile> --adapter caddyfile`. That command reads config files only,
|
|
28
|
+
so it needs no privilege and never touches certificate keys, and the runner's scoped sudo is
|
|
29
|
+
not widened. It inspects the JSON. The ask must appear under `apps.tls.automation.on_demand`,
|
|
30
|
+
which covers both Caddy 2.7's `ask` string and 2.8+'s `permission` module. The domain must
|
|
31
|
+
appear under `apps.http`. Because this is the resolved config, it doesn't matter how the operator
|
|
32
|
+
wrote the directives: an absolute or relative import, a split across imported files, a
|
|
33
|
+
`/api/gds` alias. The base path is `PROVISION_CADDYFILE`, default `/etc/caddy/Caddyfile`.
|
|
34
|
+
3. **Two checkpoints.** A preflight checks the ask before the box is touched, in both the
|
|
35
|
+
provision and domain-attach legs, beside the DNS-scope preflight. The import can only be
|
|
36
|
+
proven once a snippet exists, so a second check runs after the write and requires the domain
|
|
37
|
+
to be served. It fails the run before any reload. A snippet in a directory nothing imports
|
|
38
|
+
is inert, so leaving it on disk is safe.
|
|
39
|
+
4. **The reload is loud.** `systemctl reload caddy` no longer passes `allowFail`. Caddy's
|
|
40
|
+
graceful reload validates and swaps atomically, and it keeps the old config when it rejects
|
|
41
|
+
the new one. A failed reload fails the run and never leaves Caddy half-configured. There is
|
|
42
|
+
no separate `caddy validate` step: it would load certificate keys, which needs root, and
|
|
43
|
+
`caddy adapt` plus the reload already cover what it would catch.
|
|
44
|
+
|
|
45
|
+
## Consequences
|
|
46
|
+
|
|
47
|
+
- A box that was never set up gets a named instruction instead of a silently unreachable project.
|
|
48
|
+
- Domainless instances, dry runs and dedicated droplets (Caddy comes from cloud-init) skip the check.
|
|
49
|
+
- `caddy` must be on the runner's `PATH` on the box that serves the snippet. Every supported shape already has it there, because it reloads that Caddy.
|
package/docs/adr/README.md
CHANGED
|
@@ -394,3 +394,4 @@ This keeps the decision history honest and traceable.
|
|
|
394
394
|
| 0300 | [**A missing protected-surface registry is a broken install, not a module opt-out** ([task 1003662](https://cloudbongos.com/builders#/task/1003662), goal 1000091). Filed as a bug with a remedy attached: `require('./module-scope-map')` throws when `modules/government/protected-surfaces.json` is absent, so make it degrade to an empty protected set + a warning, matching `resolveOptional`. The throw reproduces, and the rest is wrong four times over. **The throw is not in the named file** — `module-scope-map.js` has no throw and no file read; it requires `permission-path-check.js`, whose `loadRegistry()` throws at module scope. **It is a written contract, not an oversight** — that file's header says FAIL-CLOSED in capitals and already pre-answers the layering worry (the read is `fs`, never `require`, so ADR 0083's one-way rule holds; static data, not code; `route-rank-check.js` reads `modules/*/routes/*.js` the same way). **Disabling the module cannot cause it** — enablement is a runtime flag, never a file deletion, and the read never consults it. **"Not yet materialized" cannot either** — no `files[]` allowlist, so `npm pack --dry-run` puts the 13,274-byte registry in the tarball; per ADR 0108 §1 you cannot have `@bongos/core` without it. The `resolveOptional` precedent does not transfer: that is the module **seam/port** resolver for a missing runtime *capability*, whereas this is the static data the boundary is made of, read where there is no server and no DB (a git hook, a CI job) — borrowing it converts "the boundary is unreadable" into "there is no boundary". **Decision: fail-closed stays, no behavior change, the remedy is rejected — and the one genuine defect is fixed: the message now names the cause and the fix** (reinstall the core; do not empty the set), because stating a refusal without a remedy is exactly how a reader concludes the module is optional and files this task. The asymmetry is the point: a false "nothing is protected" is silent, permanent and grants authority; a false "this install is broken" is loud and grants nothing. Revisit only if a core-only checkout without `modules/` becomes legitimate — and then the question is *where the registry lives*, not whether it is optional.](0300-a-missing-protected-surface-registry-is-a-broken-install.md) | governance / permissions |
|
|
395
395
|
| 0301 | [**A red PR names its failing tests in a commit status, and says so when it cannot see** ([task 1003988](https://cloudbongos.com/builders#/task/1003988), goal 1000095). One pull request took nine red CI runs in four hours because the builder box could not learn which test failed: without Checks:read GitHub nulls every check-run node, the server dropped the holes silently, and the red PR was reported as a review-or-pending wait. Now the merge state carries checks_unreadable and a red, unreadable rollup reads failing_check_unreadable; the unit workflow posts the failing test names as a unit-report commit STATUS (readable without the permission) from a separate job that holds the only write token and checks out no PR code; the server returns it as failing_tests, reduced to test-list vocabulary because the PR controls the workflow that wrote it.](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) | GDS / shipping pipeline |
|
|
396
396
|
| 0302 | [**The third identical failure stops the session and asks** ([task 1003990](https://cloudbongos.com/builders#/task/1003990), goal 1000095). Sessions looped without noticing: one PR re-shipped into the same red check nine times, and 45 of 169 claim attempts were the same refusal retried minutes apart. A PostToolUse hook now counts walls per session — the signature is the `[otb-plain]` card's "What happened" line + the task id + the RED line, so a red check naming different failing tests each run reads as progress — and on the third identical one injects a single instruction: stop retrying, diagnose (path named), or /ask-for-help. Waiting cards ("Nothing to do…") are never counted; git-guard and worktree-guard count their own blocks through the same helper; it never blocks a tool call and echoes nothing a tool printed. Rejected: counting any repeated failing command (red→green is that), blocking the fourth attempt (a new gate, ADR 0162), server-side detection (cannot see guard blocks or speak into the session).](0302-the-third-identical-failure-stops-the-session-and-asks.md) | GDS / sessions / hooks |
|
|
397
|
+
| 0303 | [**Provisioning verifies the base Caddyfile, and never edits it** ([task 1002860](https://cloudbongos.com/builders#/task/1002860), goal 1000106, from idea 1000524). The runner assumed the box's `/etc/caddy/Caddyfile` carried the `on_demand_tls` tls-check ask and `import /etc/caddy/sites/*.caddy`, and reloaded with `allowFail` — a box missing either provisioned "successfully" and the instance was unreachable. It now reads Caddy's RESOLVED config (`caddy adapt`: unprivileged, no key access, indifferent to how the directives are written) before touching the box (the ask) and after writing the snippet (the domain is served ⇒ the import reaches it), refusing with the exact line to add, and the reload is loud. Rejected: editing the base file (`on_demand_tls` must sit inside the global block an append cannot place, and a bad write takes down the control plane's own apex); `caddy validate` (loads certificate keys, needs root).](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md) | provisioning / infra |
|
package/docs/api/openapi.json
CHANGED
|
@@ -20991,6 +20991,7 @@
|
|
|
20991
20991
|
"token": {},
|
|
20992
20992
|
"expires_at": {},
|
|
20993
20993
|
"builder_login": {},
|
|
20994
|
+
"api_base": {},
|
|
20994
20995
|
"paste_command": {}
|
|
20995
20996
|
},
|
|
20996
20997
|
"required": [
|
|
@@ -20998,6 +20999,7 @@
|
|
|
20998
20999
|
"token",
|
|
20999
21000
|
"expires_at",
|
|
21000
21001
|
"builder_login",
|
|
21002
|
+
"api_base",
|
|
21001
21003
|
"paste_command"
|
|
21002
21004
|
]
|
|
21003
21005
|
},
|
package/docs/architecture.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **Cloud Bongos control-plane droplet (NEW 2026-07-05, [ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md), task [#2063](https://example.com/builders#/task/2063)):** DigitalOcean, hostname `cloudbongos`, IPv4 `REDACTED_IP`, NYC3, `s-1vcpu-2gb` (2 GB / 1 vCPU), Ubuntu 24.04, ~$12/mo, tag `cloudbongos-control`. Runs **cloudbongos.com + `builders.` + `status.`** (`src/platform-server.js`, Cloud Bongos brand, port 3002, own `cloudbongos` DB, checkout `/home/lars/cloudbongos-instance`, the root subscribed to the nightly core-patch sweep) and is the **sole token-holding control plane** — a SECOND root, `/home/lars/cloudbongos`, survives only to vendor `infra/` (the box cloud-init the core package does not ship, ADR 0150); it is not subscribed, so nothing may resolve core code from it (task 1002738, [ADR 0144](adr/0144-devbox-rehome-onto-cloudbongos-plane.md) follow-up 5) — `/etc/cloudbongos/box.env` holds the account-wide Cloudflare token (IP-locked to this box) + the DigitalOcean token; this is where `provision.js` provisions instance subdomains. **Structurally separate SSH** (own `cloudbongos_ed25519` key). IPv6 egress disabled so the IPv4-locked CF token can't be spuriously rejected. cloudbongos.com was previously a co-tenant on the co-hosting box; that unit was decommissioned at cutover. **Follow-ups:** deploy automation (task [#2064](https://example.com/builders#/task/2064)) + fully stripping cloud tokens off the co-hosting box (task [#2065](https://example.com/builders#/task/2065)).
|
|
15
15
|
- **Server stack:** Node.js 22 LTS · PostgreSQL 16 · Caddy 2.11.2. `example.service` (systemd) runs `node server.js` as user `lars` on `127.0.0.1:3000`; restart-on-failure.
|
|
16
16
|
- **Hardening:** `lars` user with passwordless sudo; SSH key-only (root login disabled, password auth disabled); ufw allows 22/80/443; fail2ban active; unattended-upgrades enabled; timezone `America/New_York`.
|
|
17
|
-
- **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
|
|
17
|
+
- **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`); the provisioning runner verifies the box's resolved Caddy config carries that import and the `on_demand_tls` ask before it reports a domain wired, and refuses with the line to add rather than editing the base file ([ADR 0303](adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
|
|
18
18
|
- **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label ([`infra/prometheus/prometheus.yml`](../infra/prometheus/prometheus.yml)): **cloudbongos** locally (`:3002`); **example + emersonian-circles** on the co-hosting box, reached over the **private VPC** via `socat` forwarders on `10.108.0.2:9101/9102` (→ their loopback `:3000`/`:3004`), ufw-locked to the control-plane box's private IP — raw TCP relays, so the exporter still sees a loopback request and needs no token; nothing is public. **Grafana** (`127.0.0.1:3003`, loopback-only) renders the 4 provisioned-as-code dashboards (folder **Cloud Bongos**: system-overview, error-watch, builder-activity, auth-security), each with a **`project`** template variable for per-instance / fleet views. Reverse-proxied on the `*.cloudbongos.com` origin cert — Archon-only, gated by Caddy `basic_auth` (user `archon`) **and** Grafana's own admin login (both secrets owner-held, never in the repo). Grafana config is a systemd `GF_*` env drop-in (`grafana-server.service.d/override.conf`, `chmod 600`). ~256 MB RAM. A future dedicated-droplet instance is added as a `METRICS_TOKEN` Bearer target instead of a VPC forwarder.
|
|
19
19
|
|
|
20
20
|
**Application stack:**
|
|
@@ -226,7 +226,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
|
|
|
226
226
|
- Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
|
|
227
227
|
- Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
|
|
228
228
|
- Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
|
|
229
|
-
- Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
|
|
229
|
+
- Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a dev box with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`, **NOT** `allowBoxScope` → a box-scoped session is `403 box_scope`, so the builder must `/builder-reauth` first; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (a box, or `OTB_PUSH_VIA_SERVER=1`); a laptop with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
|
|
230
230
|
- Archon monitoring reads backing the `/watch` page (ADR 0036): `GET /builders/roster`, `GET /grades/by-builder[?days=N]` (**archon-only**, [#726](https://example.com/builders#/task/726) — per-builder grade breakdown for the MARKS section; the project-wide aggregate stays public at `/public/grades`), `GET /audit-log`, `GET /override-requests`, `GET /access-requests`, `GET /security/reports`. Personal-prefs writes backing `/settings`: `GET/PATCH /me/skill-prefs`, `PATCH /me/disciplines`.
|
|
231
231
|
- Per-builder "needs" + own Gemini key ([ADR 0073](adr/0073-builder-needs-signal-and-byok-gemini-key.md), [#1013](https://example.com/builders#/task/1013)): `GET /me` now also carries `needs` ({items, action_needed_count} — the consistent "the system needs an input from you" signal; `modules/builder-settings/builder-needs.js` is the SSOT — carved out in BV1.R80, resolved by `GET /me` via the `builder-settings` kernel port — rendered by the hall Standing card + the CLI `printNeedsNudge`). The own-key (pragmatic BYOK) endpoints, all own-scoped: `GET /me/art-key/own` (masked meta — last4 only), `PUT /me/art-key/own` (validate-on-save via a Google list-models call → encrypt with `src/bongos/secret-box.js` → store; `503 storage_not_configured` until `BUILDER_SECRET_KEY` is set, `422 invalid_key` on a bad key), `DELETE /me/art-key/own`. The existing `GET /me/art-key` (shared-key box-sync delivery) is extended to also deliver the decrypted own key, which `scripts/gds/fetch-art-key.js` syncs into the local `gemini_api_key` slot (own key wins in the `gen_api.py` cascade).
|
|
232
232
|
- Builder memory (ADR 0024 cloneable memory + ADR 0026 BFG). Self-scoped (owner is always `req.builder.id`, never input): `POST /memory/sync` (push own memory; own 64 MB parser, vs the global 64 KB), `GET /memory/files`, `GET /memory/file`, `DELETE /memory/file` (the BFG delete affordance, 6C.2). Cross-builder (input-owner) endpoints — all privileged + audit-logged fail-closed: `GET /memory/builders/:id/files`+`/file` (**archon forensic** read), `GET /memory/bfg/builders/:id/files`+`/file` (**BFG-principal only**, 6B.1 read), and the one cross-builder WRITE `POST /memory/builders/:id/bfg-write` (**BFG-principal only**, kill-switched `BFG_WRITE_ENABLED` default-OFF, namespaced `bfg/`, attributed `author: BFG`, write+audit in one tx — dreams + transparent corrections, 6C.1/6C.2).
|
|
@@ -2152,5 +2152,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2152
2152
|
landed since 1.19.837 with no explicit bump. run 35250225228. (task 1002620)
|
|
2153
2153
|
1.19.839 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2154
2154
|
landed since 1.19.838 with no explicit bump. run 35252591179. (task 1002620)
|
|
2155
|
+
1.19.840 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2156
|
+
landed since 1.19.839 with no explicit bump. run 35254616836. (task 1002620)
|
|
2155
2157
|
---------------------------------------------------------------------------
|
|
2156
2158
|
```
|
|
@@ -1479,8 +1479,8 @@ module.exports = {
|
|
|
1479
1479
|
landProvenForTask,
|
|
1480
1480
|
prReferencesTask,
|
|
1481
1481
|
findTaskPullRequest,
|
|
1482
|
-
scrubCredential,
|
|
1483
|
-
truncateLoudly,
|
|
1482
|
+
scrubCredential, ghHeaders,
|
|
1483
|
+
truncateLoudly, // task 1003222 — exported so its behaviour is testable, not just its effect
|
|
1484
1484
|
STDERR_MAX,
|
|
1485
1485
|
isAncestorOfDefaultBranch,
|
|
1486
1486
|
resolveToken,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// modules/lifecycle/pr-assign.js — assign a ship's pull request to the builder who wrote
|
|
2
|
+
// it, and name them in its body (task 1003991).
|
|
3
|
+
//
|
|
4
|
+
// WHY. A builder box holds no GitHub credential (ADR 0055), so the server opens the PR
|
|
5
|
+
// with its own — and GitHub records the credential's account as the AUTHOR: 259 of the
|
|
6
|
+
// last 300 PRs. The builder's commits are theirs, but "created by me" is empty for them,
|
|
7
|
+
// and the PR is where CI and merge state live. An ASSIGNEE is the one PR-level link to a
|
|
8
|
+
// person the server can set for someone else; "assigned to me" then finds their work.
|
|
9
|
+
//
|
|
10
|
+
// A COURTESY, NEVER A GATE. assignPullRequest resolves { assigned, reason? } on every
|
|
11
|
+
// path and never rejects: a login GitHub will not assign (not a collaborator — it
|
|
12
|
+
// answers 201 and silently drops the name), a missing permission or a network fault
|
|
13
|
+
// must not be able to fail a ship that already pushed. A sibling of github-push.js
|
|
14
|
+
// rather than a function in it because that file sits at the line cap.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const githubPush = require('./github-push');
|
|
19
|
+
|
|
20
|
+
// GitHub's own login rule: 1–39 chars, alphanumeric with single inner hyphens — plus
|
|
21
|
+
// the underscore a managed-user login carries (`name_shortcode`). The login is
|
|
22
|
+
// interpolated into a PR body as an @-mention, so anything else never gets that far.
|
|
23
|
+
const GITHUB_LOGIN_RE = /^[a-z\d](?:[a-z\d]|[-_](?=[a-z\d])){0,38}$/i;
|
|
24
|
+
// The assignment is the one OPTIONAL call between a finished push and the route's 201:
|
|
25
|
+
// a stalled GitHub must not make a ship that succeeded read as a failed publish.
|
|
26
|
+
const ASSIGN_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
function isGithubLogin(login) {
|
|
29
|
+
return typeof login === 'string' && GITHUB_LOGIN_RE.test(login);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The sentence the PR body carries, or '' when there is no usable login.
|
|
33
|
+
function prBuilderLine(login) {
|
|
34
|
+
return isGithubLogin(login) ? `Built by @${login}.` : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function logAssignFailure(reason, number, logImpl) {
|
|
38
|
+
try {
|
|
39
|
+
const sink = logImpl || require('../../src/module-api').logger('lifecycle');
|
|
40
|
+
sink.warn('pull request could not be assigned to its builder (the ship is unaffected)',
|
|
41
|
+
{ reason: githubPush.scrubCredential(String(reason)), pr: number });
|
|
42
|
+
} catch (_) { /* logging is never load-bearing */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function assignPullRequest({ number, login }, deps = {}) {
|
|
46
|
+
const miss = (reason, detail) => {
|
|
47
|
+
logAssignFailure(detail ? `${reason}: ${detail}` : reason, number, deps.logImpl);
|
|
48
|
+
return { assigned: false, reason };
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
if (!isGithubLogin(login)) return miss('bad_login');
|
|
52
|
+
if (!Number.isInteger(number) || number <= 0) return miss('bad_pr_number');
|
|
53
|
+
const token = await githubPush.resolveToken(deps);
|
|
54
|
+
const repoInfo = deps.repoInfo || require('../../src/module-api').repoInfo.loadRepoInfo();
|
|
55
|
+
if (!token || !repoInfo || repoInfo.error) return miss('unconfigured');
|
|
56
|
+
|
|
57
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
58
|
+
const res = await doFetch(
|
|
59
|
+
`https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/issues/${number}/assignees`,
|
|
60
|
+
{
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { ...githubPush.ghHeaders(token), 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ assignees: [login] }),
|
|
64
|
+
signal: AbortSignal.timeout(ASSIGN_TIMEOUT_MS),
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
if (!res.ok) return miss(`http_${res.status}`);
|
|
68
|
+
const issue = await res.json();
|
|
69
|
+
const took = ((issue && issue.assignees) || [])
|
|
70
|
+
.some((a) => String(a && a.login).toLowerCase() === login.toLowerCase());
|
|
71
|
+
return took ? { assigned: true } : miss('not_assignable');
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return miss('threw', (e && e.message) || '');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { assignPullRequest, prBuilderLine };
|
|
@@ -46,6 +46,7 @@ const { makeAbandonHandler } = require('./abandon-handler');
|
|
|
46
46
|
// longer imports the grader — it resolves the `grade` kernel PORT at grade time
|
|
47
47
|
// (seams.resolveOptional('grade'), in POST /tasks/:id/grade below).
|
|
48
48
|
const githubPush = require('../github-push');
|
|
49
|
+
const prAssign = require('../pr-assign');
|
|
49
50
|
const shipAssurance = require('../ship-assurance');
|
|
50
51
|
const RECONCILE_PROOF = Object.freeze({ kind: 'proof', reason: 'publish-status reconcile: land proven' });
|
|
51
52
|
const seams = api;
|
|
@@ -1125,12 +1126,25 @@ module.exports = function buildTasksRouter() {
|
|
|
1125
1126
|
fs.writeFileSync(bundlePath, bundleBuf);
|
|
1126
1127
|
const { head_sha: pushedSha } = await githubPush.pushBundle({ bundlePath, branch, headSha });
|
|
1127
1128
|
const title = `task ${id}: ${deHashRefs(valueSummary) || branch}`.slice(0, 240);
|
|
1129
|
+
// task 1003991: the server's credential is the PR's AUTHOR, so the builder who
|
|
1130
|
+
// wrote the work is named in the body and made its assignee. The claim holder,
|
|
1131
|
+
// not the caller — an Archon re-shipping someone's strand did not write it. A
|
|
1132
|
+
// lookup that FAILS names nobody (a PR body is never rewritten, so a wrong name
|
|
1133
|
+
// is permanent); only a never-claimed task falls back to the caller.
|
|
1134
|
+
const builderLogin = await db.getLastClaimHolder(id)
|
|
1135
|
+
.then((h) => (h ? h.github_login : req.builder.github_login), () => null);
|
|
1136
|
+
const builderLine = prAssign.prBuilderLine(builderLogin);
|
|
1128
1137
|
const body =
|
|
1129
|
-
`Auto-opened by GDS server-mediated publish (ADR 0055) for task ${id}
|
|
1138
|
+
`Auto-opened by GDS server-mediated publish (ADR 0055) for task ${id}.` +
|
|
1139
|
+
`${builderLine ? ` ${builderLine}` : ''}\n\n` +
|
|
1130
1140
|
`${deHashRefs(valueSummary) || '(no value summary)'}\n\n` +
|
|
1131
1141
|
`Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
|
|
1132
1142
|
`Auto-merge is enabled — GitHub lands it once the required checks pass.`;
|
|
1133
1143
|
const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
|
|
1144
|
+
// Best-effort by contract (pr-assign.js never rejects); the catch is for a future
|
|
1145
|
+
// edit that forgets — an assignment must not be able to fail a pushed ship.
|
|
1146
|
+
const assignment = await prAssign.assignPullRequest({ number: pr.number, login: builderLogin })
|
|
1147
|
+
.catch(() => ({ assigned: false }));
|
|
1134
1148
|
// task 1001431: ask WHY when arming fails. `auto_merge: false` on its own told the
|
|
1135
1149
|
// builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
|
|
1136
1150
|
// a permanent condition that reads exactly like a transient blip.
|
|
@@ -1157,6 +1171,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1157
1171
|
}
|
|
1158
1172
|
return res.status(201).json({
|
|
1159
1173
|
ok: true, pr_url: pr.url, pr_number: pr.number, head_sha: pushedSha, auto_merge: autoMerge,
|
|
1174
|
+
pr_assignee: assignment.assigned ? builderLogin : null,
|
|
1160
1175
|
// Only present when arming failed — the publish itself still succeeded (201).
|
|
1161
1176
|
...(autoMerge ? {} : { auto_merge_reason: autoMergeResult.reason }),
|
|
1162
1177
|
});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.840",
|
|
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.840",
|
|
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.840",
|
|
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",
|