@bongos/core 1.19.969 → 1.19.970
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 +26 -21
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/abandon.js +8 -4
- package/scripts/gds/cli-lib.js +51 -0
- package/scripts/gds/demote.js +9 -4
- package/scripts/gds/login.js +6 -2
- package/scripts/gds/provision-repo.js +36 -3
- package/scripts/gds/release.js +11 -3
- package/scripts/gds/setup.js +2 -2
- package/scripts/gds/start.js +7 -3
- package/src/module-api.js +1 -1
- package/tests/cli_err_code.mjs +148 -0
- package/tests/plain_cards.mjs +5 -1
- package/tests/provision.mjs +105 -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.970",
|
|
6
|
+
"core_contract": "1.19.970",
|
|
7
|
+
"source_commit": "6fcd89630051b6a8a27ab995131716370ed9082e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-23T01:02:05.515Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 515,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2370,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2911,
|
|
20
|
+
"tree_sha256": "60a4632de03ed24bd98cbfe694a93acfed70e02c72838da91213a9c68ad14813",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2697,7 +2697,7 @@
|
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "docs/module-api-changelog.md",
|
|
2699
2699
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2700
|
+
"sha256": "e842c06ae274c61f5f38063af7b5eb5da4798954df2b9e3e42c1929352dd9a3f"
|
|
2701
2701
|
},
|
|
2702
2702
|
{
|
|
2703
2703
|
"path": "docs/modules-contract.md",
|
|
@@ -8107,12 +8107,12 @@
|
|
|
8107
8107
|
{
|
|
8108
8108
|
"path": "package-lock.json",
|
|
8109
8109
|
"mode": "0000644",
|
|
8110
|
-
"sha256": "
|
|
8110
|
+
"sha256": "b398350d28d5e528b46daafbadd4b19ddd9e9423a0c234aaabd27dc1718d6864"
|
|
8111
8111
|
},
|
|
8112
8112
|
{
|
|
8113
8113
|
"path": "package.json",
|
|
8114
8114
|
"mode": "0000644",
|
|
8115
|
-
"sha256": "
|
|
8115
|
+
"sha256": "03e808eb0d1d2cb9be5d0ee13830a79fd5b0752ee5ff7f0a17993df3dc147903"
|
|
8116
8116
|
},
|
|
8117
8117
|
{
|
|
8118
8118
|
"path": "public-docs/index.html",
|
|
@@ -8162,7 +8162,7 @@
|
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "scripts/gds/abandon.js",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "a1246dd85fe90e105561d4069bd65a79fc1b516addbbeea22c69ce3fcf1e19ab"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "scripts/gds/accessibility-replay.js",
|
|
@@ -8472,7 +8472,7 @@
|
|
|
8472
8472
|
{
|
|
8473
8473
|
"path": "scripts/gds/cli-lib.js",
|
|
8474
8474
|
"mode": "0000644",
|
|
8475
|
-
"sha256": "
|
|
8475
|
+
"sha256": "013a3ff00e01912efabe65d58d5c2cbff93bd61ad052e645d03c034aff203ddb"
|
|
8476
8476
|
},
|
|
8477
8477
|
{
|
|
8478
8478
|
"path": "scripts/gds/client-baseurl-guard.js",
|
|
@@ -8542,7 +8542,7 @@
|
|
|
8542
8542
|
{
|
|
8543
8543
|
"path": "scripts/gds/demote.js",
|
|
8544
8544
|
"mode": "0000644",
|
|
8545
|
-
"sha256": "
|
|
8545
|
+
"sha256": "3b80acbc43ce96e07f11e6531c35499bc9a5bf8e4103d57e37c83169afdf0087"
|
|
8546
8546
|
},
|
|
8547
8547
|
{
|
|
8548
8548
|
"path": "scripts/gds/deploy-divergence.js",
|
|
@@ -8912,7 +8912,7 @@
|
|
|
8912
8912
|
{
|
|
8913
8913
|
"path": "scripts/gds/login.js",
|
|
8914
8914
|
"mode": "0000644",
|
|
8915
|
-
"sha256": "
|
|
8915
|
+
"sha256": "0d719e43955fdbcc40aee500438db5b31c6c006490e90d99caadf31b215196f7"
|
|
8916
8916
|
},
|
|
8917
8917
|
{
|
|
8918
8918
|
"path": "scripts/gds/main-audit.js",
|
|
@@ -9087,7 +9087,7 @@
|
|
|
9087
9087
|
{
|
|
9088
9088
|
"path": "scripts/gds/provision-repo.js",
|
|
9089
9089
|
"mode": "0000644",
|
|
9090
|
-
"sha256": "
|
|
9090
|
+
"sha256": "6e025cfa37165ff491f736f31bb97fa439e3e6cece9eaeeb4edcd6698e9868c0"
|
|
9091
9091
|
},
|
|
9092
9092
|
{
|
|
9093
9093
|
"path": "scripts/gds/provision-teardown.js",
|
|
@@ -9162,7 +9162,7 @@
|
|
|
9162
9162
|
{
|
|
9163
9163
|
"path": "scripts/gds/release.js",
|
|
9164
9164
|
"mode": "0000644",
|
|
9165
|
-
"sha256": "
|
|
9165
|
+
"sha256": "5c260296937727b4445f4df0b851ba174334b7680b6cad6243cc94b73d3934c4"
|
|
9166
9166
|
},
|
|
9167
9167
|
{
|
|
9168
9168
|
"path": "scripts/gds/rename-goals.js",
|
|
@@ -9427,7 +9427,7 @@
|
|
|
9427
9427
|
{
|
|
9428
9428
|
"path": "scripts/gds/setup.js",
|
|
9429
9429
|
"mode": "0000644",
|
|
9430
|
-
"sha256": "
|
|
9430
|
+
"sha256": "156e63c837a5b3bd57ac3f76c4770ef73ad469d27a6f03f8b8a617b57f4020d0"
|
|
9431
9431
|
},
|
|
9432
9432
|
{
|
|
9433
9433
|
"path": "scripts/gds/ship-check.js",
|
|
@@ -9602,7 +9602,7 @@
|
|
|
9602
9602
|
{
|
|
9603
9603
|
"path": "scripts/gds/start.js",
|
|
9604
9604
|
"mode": "0000644",
|
|
9605
|
-
"sha256": "
|
|
9605
|
+
"sha256": "36c814dcf1b233f2279f1a7b06a337dea0d1799119acbfac004af88c6e1a56d1"
|
|
9606
9606
|
},
|
|
9607
9607
|
{
|
|
9608
9608
|
"path": "scripts/gds/status.js",
|
|
@@ -10122,7 +10122,7 @@
|
|
|
10122
10122
|
{
|
|
10123
10123
|
"path": "src/module-api.js",
|
|
10124
10124
|
"mode": "0000644",
|
|
10125
|
-
"sha256": "
|
|
10125
|
+
"sha256": "a16e899a5c50b65af3e0976a27e72e2f1e50b7cfc8a8acafc64b19cebae1eb93"
|
|
10126
10126
|
},
|
|
10127
10127
|
{
|
|
10128
10128
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10914,6 +10914,11 @@
|
|
|
10914
10914
|
"mode": "0000644",
|
|
10915
10915
|
"sha256": "3e3e17428a6c8f50ce0512cfacc4e18e0b6a2c506d185fa5f2adf21c9feffe9f"
|
|
10916
10916
|
},
|
|
10917
|
+
{
|
|
10918
|
+
"path": "tests/cli_err_code.mjs",
|
|
10919
|
+
"mode": "0000644",
|
|
10920
|
+
"sha256": "ec9d2cc2054545b3fc886a9b7168ca851acf4cfb7937fab6e065e8fd7ed5eeb4"
|
|
10921
|
+
},
|
|
10917
10922
|
{
|
|
10918
10923
|
"path": "tests/cli_exit_no_abort.mjs",
|
|
10919
10924
|
"mode": "0000644",
|
|
@@ -12972,7 +12977,7 @@
|
|
|
12972
12977
|
{
|
|
12973
12978
|
"path": "tests/plain_cards.mjs",
|
|
12974
12979
|
"mode": "0000644",
|
|
12975
|
-
"sha256": "
|
|
12980
|
+
"sha256": "90310dd37d97dff67feaed377c2db4efdf906902f5c0fa8f49ee721941c2b272"
|
|
12976
12981
|
},
|
|
12977
12982
|
{
|
|
12978
12983
|
"path": "tests/planning_session_skill.mjs",
|
|
@@ -13202,7 +13207,7 @@
|
|
|
13202
13207
|
{
|
|
13203
13208
|
"path": "tests/provision.mjs",
|
|
13204
13209
|
"mode": "0000644",
|
|
13205
|
-
"sha256": "
|
|
13210
|
+
"sha256": "58dd9185b37e9f5654564a5c700e51995b303f26af9eaae3b4b4d3ee29351864"
|
|
13206
13211
|
},
|
|
13207
13212
|
{
|
|
13208
13213
|
"path": "tests/provision_account_identity.mjs",
|
|
@@ -2412,5 +2412,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2412
2412
|
landed since 1.19.967 with no explicit bump. run 35797263467. (task 1002620)
|
|
2413
2413
|
1.19.969 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2414
2414
|
landed since 1.19.968 with no explicit bump. run 35798488315. (task 1002620)
|
|
2415
|
+
1.19.970 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2416
|
+
landed since 1.19.969 with no explicit bump. run 35804568036. (task 1002620)
|
|
2415
2417
|
---------------------------------------------------------------------------
|
|
2416
2418
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.970",
|
|
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.970",
|
|
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.970",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/abandon.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// the ledger stays complete. Retired the wrong task? POST /tasks/<id>/promote
|
|
18
18
|
// restores it to backlog at the same rank (db.restoreTask) — no Archon SQL needed.
|
|
19
19
|
|
|
20
|
-
const { cliClient, requireSession, arg } = require('./cli-lib');
|
|
20
|
+
const { errCode, cliClient, requireSession, arg } = require('./cli-lib');
|
|
21
21
|
// task 1003153 — the live-status set, shared with the dead-edge detector so
|
|
22
22
|
// "who did this cut strand" means the same thing in both places.
|
|
23
23
|
const { LIVE_STATUSES } = require('../../modules/lifecycle/dead-deps.js');
|
|
@@ -68,12 +68,16 @@ function commitsAheadOfMain(branch) {
|
|
|
68
68
|
const api = await cliClient();
|
|
69
69
|
const r = await api.tasks.postTasksIdAbandon({ id: taskId, body: { reason } });
|
|
70
70
|
if (!r.ok) {
|
|
71
|
-
|
|
71
|
+
// ADR 0116 nests the slug at error.code; a flat `r.data.error === '…'` compares an
|
|
72
|
+
// object to a string and never matches, so every branch below was unreachable
|
|
73
|
+
// against a live server (task 1003658).
|
|
74
|
+
const code = errCode(r.data);
|
|
75
|
+
if (code === 'cannot_abandon_shipped') {
|
|
72
76
|
console.error(`Task #${taskId} is shipped — shipped work is immutable history and cannot be abandoned.`);
|
|
73
|
-
} else if (
|
|
77
|
+
} else if (code === 'task_has_active_claim') {
|
|
74
78
|
console.error(`Task #${taskId} has an active claim. Release it first:`);
|
|
75
79
|
console.error(` node scripts/gds/release.js ${taskId}`);
|
|
76
|
-
} else if (
|
|
80
|
+
} else if (code === 'task_not_found') {
|
|
77
81
|
console.error(`Task #${taskId} not found.`);
|
|
78
82
|
} else if (r.status === 403) {
|
|
79
83
|
console.error('Abandoning a task needs Metic+ rank (ADR 0152).');
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -1098,7 +1098,58 @@ function cardFlagsFromMe(me) {
|
|
|
1098
1098
|
return { level: null, showCard: true, showWidget: enabled, expanded: false };
|
|
1099
1099
|
}
|
|
1100
1100
|
|
|
1101
|
+
// ---------------------------------------------------------------------------
|
|
1102
|
+
// errCode — read the error slug out of an ADR 0116 response envelope.
|
|
1103
|
+
// ---------------------------------------------------------------------------
|
|
1104
|
+
// ADR 0116 standardised every failure on a NESTED envelope:
|
|
1105
|
+
//
|
|
1106
|
+
// { "error": { "code": "task_not_found", "message": "…", "details": … } }
|
|
1107
|
+
//
|
|
1108
|
+
// so the obvious-looking `r.data.error === 'task_not_found'` compares an OBJECT to a
|
|
1109
|
+
// STRING and is never true. That is not a cosmetic bug: each such comparison guards a
|
|
1110
|
+
// helpful, specific message, so against a live server every one of those branches is
|
|
1111
|
+
// unreachable and the user falls through to the raw `(409): [object Object]` dump —
|
|
1112
|
+
// the failure looks like a tooling crash rather than the clear refusal the server sent.
|
|
1113
|
+
//
|
|
1114
|
+
// Verified live 2026-09-22 against cloudbongos.com:
|
|
1115
|
+
// POST /api/bongos/tasks/1003846/abandon
|
|
1116
|
+
// -> 409 {"error":{"code":"cannot_abandon_shipped","message":"shipped tasks are immutable history"}}
|
|
1117
|
+
//
|
|
1118
|
+
// The flat-string fallback is deliberate, not defensive padding: some older routes and
|
|
1119
|
+
// every non-envelope transport error (a proxy's plain-text body, a pre-0116 instance an
|
|
1120
|
+
// upgraded CLI is talking to) still surface `error` as a bare string. Reading both is
|
|
1121
|
+
// what makes one helper safe to use everywhere.
|
|
1122
|
+
//
|
|
1123
|
+
// Fixed in ship.js under task 1002789; extracted here under task 1003658 so the next
|
|
1124
|
+
// caller cannot reintroduce the same comparison. Pure — tests/cli_err_code.mjs.
|
|
1125
|
+
function errCode(data) {
|
|
1126
|
+
const e = data && data.error;
|
|
1127
|
+
if (typeof e === 'string') return e;
|
|
1128
|
+
if (e && typeof e === 'object' && typeof e.code === 'string') return e.code;
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// errDetails — the structured payload beside the slug, same envelope.
|
|
1133
|
+
//
|
|
1134
|
+
// This matters as much as errCode and is easy to miss: the sibling fields moved WITH
|
|
1135
|
+
// the code. demote.js printed `r.data.current_status` beside its message; ADR 0116 put
|
|
1136
|
+
// that at `error.details.current_status`, so a fix that changed only the comparison
|
|
1137
|
+
// would have restored the branch and then printed "it is 'undefined', not 'ready'" —
|
|
1138
|
+
// a half-fix that looks like a whole one. Verified live 2026-09-22:
|
|
1139
|
+
//
|
|
1140
|
+
// POST /api/bongos/tasks/1003846/demote
|
|
1141
|
+
// -> 409 {"error":{"code":"cannot_demote","details":{"current_status":"shipped"}}}
|
|
1142
|
+
//
|
|
1143
|
+
// Falls back to the top-level body so a pre-0116 flat response still resolves.
|
|
1144
|
+
function errDetails(data) {
|
|
1145
|
+
const e = data && data.error;
|
|
1146
|
+
if (e && typeof e === 'object' && e.details && typeof e.details === 'object') return e.details;
|
|
1147
|
+
return (data && typeof data === 'object') ? data : {};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1101
1150
|
module.exports = {
|
|
1151
|
+
errCode,
|
|
1152
|
+
errDetails,
|
|
1102
1153
|
API_BASE,
|
|
1103
1154
|
detectWorktreeName,
|
|
1104
1155
|
hallBase,
|
package/scripts/gds/demote.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// prod. Strictly ready -> backlog — the server refuses any other status (a
|
|
10
10
|
// claimed/active task, or a later lifecycle state). Metic+ only (mirrors promote).
|
|
11
11
|
|
|
12
|
-
const { cliClient, requireSession } = require('./cli-lib');
|
|
12
|
+
const { cliClient, errCode, errDetails, requireSession } = require('./cli-lib');
|
|
13
13
|
|
|
14
14
|
(async () => {
|
|
15
15
|
await requireSession();
|
|
@@ -23,10 +23,15 @@ const { cliClient, requireSession } = require('./cli-lib');
|
|
|
23
23
|
const api = await cliClient();
|
|
24
24
|
const r = await api.tasks.postTasksIdDemote({ id: taskId });
|
|
25
25
|
if (!r.ok) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// ADR 0116 nests BOTH the slug and its payload — see the note in cli-lib errCode
|
|
27
|
+
// (task 1003658). current_status moved to error.details with the code, so reading
|
|
28
|
+
// it off the top level printed 'undefined' even once the branch was reachable.
|
|
29
|
+
const code = errCode(r.data);
|
|
30
|
+
const details = errDetails(r.data);
|
|
31
|
+
if (code === 'cannot_demote') {
|
|
32
|
+
console.error(`Cannot demote task #${taskId}: it is '${details.current_status}', not 'ready'.`);
|
|
28
33
|
console.error("demote only moves a task ready -> backlog (the inverse of promote).");
|
|
29
|
-
} else if (
|
|
34
|
+
} else if (code === 'task_not_found') {
|
|
30
35
|
console.error(`No task #${taskId}.`);
|
|
31
36
|
} else {
|
|
32
37
|
console.error(`demote failed (${r.status}):`, r.data);
|
package/scripts/gds/login.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
const readline = require('node:readline');
|
|
24
24
|
const { spawnSync } = require('node:child_process');
|
|
25
|
-
const { saveSession, loadStoredSession, listStoredSessions } = require('./cli-lib');
|
|
25
|
+
const { errCode, saveSession, loadStoredSession, listStoredSessions } = require('./cli-lib');
|
|
26
26
|
// The device-flow refusal detector + its plain-language cure (task 1003881). A leaf
|
|
27
27
|
// module, NOT setup.js: setup requires this file for runDeviceFlow, so requiring it
|
|
28
28
|
// back would close a require cycle. Both callers depend on the leaf instead.
|
|
@@ -104,7 +104,11 @@ async function runDeviceFlow(base, { log, idp = null }) {
|
|
|
104
104
|
// assertion scoped to the instance the user actually typed (ADR 0141 §1).
|
|
105
105
|
...(idp ? { body: JSON.stringify({ origin: base }) } : {}),
|
|
106
106
|
});
|
|
107
|
-
|
|
107
|
+
// ADR 0116 nests the slug at error.code (task 1003658). This site was half-converted:
|
|
108
|
+
// it read the nested shape for idp_signing_unconfigured but kept a FLAT comparison for
|
|
109
|
+
// auth_not_configured, so that arm was dead against a live server. errCode reads both.
|
|
110
|
+
const startCode = errCode(start.data);
|
|
111
|
+
if (start.status === 503 || startCode === 'auth_not_configured' || startCode === 'idp_signing_unconfigured') {
|
|
108
112
|
throw new Error('that instance has no sign-in configured yet — ask its Archon to set it up.');
|
|
109
113
|
}
|
|
110
114
|
// Device Flow is OFF on every GitHub App born from a manifest, and GitHub exposes no
|
|
@@ -871,6 +871,15 @@ function deployKeyPath(inst) { return path.posix.join(standaloneRoot(inst), '.gi
|
|
|
871
871
|
// github.com and core.sshCommand pinned to the key with -F /dev/null so no
|
|
872
872
|
// ~/.ssh/config identity can shadow it. Public repos return untouched; no token →
|
|
873
873
|
// skip (the request-time refusal is task 1002909's preflight). Dry-run plans only.
|
|
874
|
+
//
|
|
875
|
+
// `deps.intendedPrivate` (task 1004167) declares that the repo is ABOUT to be made
|
|
876
|
+
// private, so the wiring runs before the flip rather than after it. Opt-in, and it
|
|
877
|
+
// widens only WHEN we wire: the key stays read-only and repository ADMIN is still
|
|
878
|
+
// required. It also HARDENS the two soft skips — no credential, GitHub unreachable —
|
|
879
|
+
// into refusals, because a caller that has committed to flipping cannot be handed an
|
|
880
|
+
// unwired checkout. NO PRODUCTION CALLER SETS IT YET: the flip route and its
|
|
881
|
+
// provisioning intent are splits (b) + (c) of task 1003078, which need a migration
|
|
882
|
+
// this deliberately does not. The seam ships first so the ordering is settled.
|
|
874
883
|
async function ensurePrivateRepoAccess(inst, deps) {
|
|
875
884
|
const { log, apply } = deps;
|
|
876
885
|
const exec = deps.controlExec || deps.exec;
|
|
@@ -881,11 +890,19 @@ async function ensurePrivateRepoAccess(inst, deps) {
|
|
|
881
890
|
// preflight is apply-gated for the same reason): no token read, no GitHub call —
|
|
882
891
|
// a plan run must stay a safe inspection tool for a snagged instance.
|
|
883
892
|
if (!apply) {
|
|
884
|
-
log(
|
|
893
|
+
log(deps.intendedPrivate === true
|
|
894
|
+
? ` [repo-auth] would treat ${repo.owner}/${repo.name} as ABOUT TO BE MADE PRIVATE (pre-flip) and mint + register a read-only deploy key, wiring origin + core.sshCommand to it before the flip (ADR 0176)`
|
|
895
|
+
: ` [repo-auth] would check ${repo.owner}/${repo.name} visibility with the owner's credential and, if PRIVATE, mint + register a read-only deploy key and wire origin + core.sshCommand to it (ADR 0176)`);
|
|
885
896
|
return { planned: true };
|
|
886
897
|
}
|
|
887
898
|
const ownerToken = await resolveOwnerGithubToken(inst, deps);
|
|
888
899
|
if (!ownerToken) {
|
|
900
|
+
// With an intent declared the caller is ABOUT to flip, so a soft skip hands back
|
|
901
|
+
// an unwired checkout and the flip then breaks the recurring pull for good. The
|
|
902
|
+
// same missing credential is only a warning when nobody has committed to flipping.
|
|
903
|
+
if (deps.intendedPrivate === true) {
|
|
904
|
+
throw new Error(`${repo.owner}/${repo.name} cannot be made private: there is no usable GitHub credential stored for this project, and without one its deploy key cannot be registered — which would break the next deploy. Reconnect the project's GitHub access, then try again.`);
|
|
905
|
+
}
|
|
889
906
|
log(' [repo-auth] no usable owner GitHub credential (none stored, or it could not be read) — a PRIVATE repo would fail at the deploy pull (public repos are unaffected)');
|
|
890
907
|
return { skipped: 'no_token' };
|
|
891
908
|
}
|
|
@@ -898,6 +915,11 @@ async function ensurePrivateRepoAccess(inst, deps) {
|
|
|
898
915
|
// Plain sentence: this rides error_note onto the owner's project card.
|
|
899
916
|
throw new Error(`repository not found: GitHub answered 404 for ${repo.owner}/${repo.name} even with your granted credential — the repository may have been renamed or deleted, or the credential lost access. Check it on GitHub, then try again.`);
|
|
900
917
|
}
|
|
918
|
+
// Same asymmetry as the missing credential above: soft when nothing is committed,
|
|
919
|
+
// hard once the caller has declared the flip.
|
|
920
|
+
if (deps.intendedPrivate === true) {
|
|
921
|
+
throw new Error(`${repo.owner}/${repo.name} cannot be made private right now: GitHub could not be reached to set up its deploy key (${e && e.message}). Nothing has been changed — try again.`);
|
|
922
|
+
}
|
|
901
923
|
log(` [repo-auth] could not read ${repo.owner}/${repo.name} (${e && e.message}) — continuing; a PRIVATE repo would fail at the deploy pull`);
|
|
902
924
|
return { skipped: 'github_unreachable' };
|
|
903
925
|
}
|
|
@@ -907,7 +929,16 @@ async function ensurePrivateRepoAccess(inst, deps) {
|
|
|
907
929
|
const title = deployKeyTitle(inst);
|
|
908
930
|
const fsImpl = deps.fsImpl || fs;
|
|
909
931
|
|
|
910
|
-
|
|
932
|
+
// A flip-to-private must wire BEFORE it flips — a deploy key authenticates fine
|
|
933
|
+
// against a still-public repo, so wiring first leaves no broken window, while
|
|
934
|
+
// flipping first breaks the very next deploy pull. At that moment GitHub still
|
|
935
|
+
// answers `private: false`, so the observed value is the wrong thing to branch on:
|
|
936
|
+
// untouched, this takes the public path and (worse, if a key file exists) actively
|
|
937
|
+
// UN-wires. `intendedPrivate` is the caller declaring where the repo is going.
|
|
938
|
+
// Opt-in: absent, every path below is exactly what it was.
|
|
939
|
+
const treatPrivate = info.private || deps.intendedPrivate === true;
|
|
940
|
+
|
|
941
|
+
if (!treatPrivate) {
|
|
911
942
|
// Flipped private → public since we wired it: un-wire, or the checkout stays
|
|
912
943
|
// pinned (IdentitiesOnly + -F /dev/null admit no other identity) to a key the
|
|
913
944
|
// owner is now free to delete from their repo settings. Keyed on our key file
|
|
@@ -933,7 +964,9 @@ async function ensurePrivateRepoAccess(inst, deps) {
|
|
|
933
964
|
throw new Error(`repository access is not admin: your granted credential can read ${repo.owner}/${repo.name}, but registering its deploy key needs repository ADMIN. Ask a repository admin to raise your access (or make the repository public), then try again.`);
|
|
934
965
|
}
|
|
935
966
|
|
|
936
|
-
log(
|
|
967
|
+
log(info.private
|
|
968
|
+
? ` [repo-auth] ${repo.owner}/${repo.name} is PRIVATE — wiring its own read-only deploy key (ADR 0176)`
|
|
969
|
+
: ` [repo-auth] ${repo.owner}/${repo.name} is ABOUT TO BE MADE PRIVATE — wiring its own read-only deploy key first (ADR 0176)`);
|
|
937
970
|
if (!fsImpl.existsSync(keyPath) || !fsImpl.existsSync(`${keyPath}.pub`)) {
|
|
938
971
|
exec(`rm -f ${keyPath} ${keyPath}.pub`, { allowFail: true });
|
|
939
972
|
exec(`ssh-keygen -q -t ed25519 -N '' -C ${title} -f ${keyPath}`);
|
package/scripts/gds/release.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const { execFileSync } = require('node:child_process');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
-
const { cliClient, requireSession, arg } = require('./cli-lib');
|
|
11
|
+
const { errCode, errDetails, cliClient, requireSession, arg } = require('./cli-lib');
|
|
12
12
|
const { parkedMainCheckoutWarning } = require('./worktree-claim-guard');
|
|
13
13
|
const { plainCardLines } = require('./plain-cards');
|
|
14
14
|
|
|
@@ -96,8 +96,16 @@ function mainCheckoutRoot() {
|
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
98
|
if (!r.ok) {
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// ADR 0116 nests the slug at error.code AND its payload at error.details — see the
|
|
100
|
+
// note in abandon.js (task 1003658). claimWorktree reaches the wire through
|
|
101
|
+
// `res.fail(err.code, status, { ...err })` in modules/lifecycle/routes/claims.js:612,
|
|
102
|
+
// so it lands in details, not at the top level. Reading it off r.data directly made
|
|
103
|
+
// `wt` undefined and silently dropped the worktree name from the message — the exact
|
|
104
|
+
// half-fix this task exists to avoid, caught by the grader on the first round.
|
|
105
|
+
const code = errCode(r.data);
|
|
106
|
+
const details = errDetails(r.data);
|
|
107
|
+
if (code === 'CLAIM_SESSION_MISMATCH') {
|
|
108
|
+
const wt = details.claimWorktree;
|
|
101
109
|
console.error(`This claim was created in a different session${wt ? ` (worktree: ${wt})` : ''}.`);
|
|
102
110
|
console.error('Only the original session can release it.');
|
|
103
111
|
console.error('If that session is gone, re-run with --force to override.');
|
package/scripts/gds/setup.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// home directory) and prints the builder profile.
|
|
20
20
|
|
|
21
21
|
const readline = require('readline');
|
|
22
|
-
const { cliClient, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase, surfaceAction, isTerminalSurface } = require('./cli-lib');
|
|
22
|
+
const { cliClient, errCode, saveSession, SESSION_PATH, loadSession, printOnboardingNudge, API_BASE, hallBase, surfaceAction, isTerminalSurface } = require('./cli-lib');
|
|
23
23
|
|
|
24
24
|
// The instance's status dashboard, from branding — never a hardcoded host, so a
|
|
25
25
|
// non-OTB instance points its builders at ITS OWN dashboard (task 1002428).
|
|
@@ -566,7 +566,7 @@ async function main() {
|
|
|
566
566
|
process.exit(0);
|
|
567
567
|
}
|
|
568
568
|
console.error(`device/start failed (${start.status}):`, start.data);
|
|
569
|
-
if (start.data
|
|
569
|
+
if (errCode(start.data) === 'auth_not_configured') { // ADR 0116 nests the slug (task 1003658)
|
|
570
570
|
console.error('');
|
|
571
571
|
console.error('The server does not yet have GitHub OAuth credentials.');
|
|
572
572
|
console.error('An Archon needs to register the GitHub OAuth App and provision credentials on the server.');
|
package/scripts/gds/start.js
CHANGED
|
@@ -67,7 +67,7 @@ const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
|
67
67
|
if (_fault) { console.error(_fault); process.exit(1); }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError, cardFlagsFromMe } = require('./cli-lib');
|
|
70
|
+
const { cliClient, errCode, errDetails, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError, cardFlagsFromMe } = require('./cli-lib');
|
|
71
71
|
const { plainCardLines } = require('./plain-cards');
|
|
72
72
|
const { contextPack, formatContextPackCompact } = require('./context-pack');
|
|
73
73
|
const { depsFinding } = require('./preflight');
|
|
@@ -723,9 +723,13 @@ async function main() {
|
|
|
723
723
|
// #766: the primer-read gate on the claimable endpoint was removed, so a
|
|
724
724
|
// newcomer no longer gets a 403 stage_required here. A generic stage_required
|
|
725
725
|
// (any future onboarding gate) still renders cleanly rather than crashing.
|
|
726
|
-
|
|
726
|
+
// ADR 0116 nests the slug and its payload — see the note in cli-lib errCode (task
|
|
727
|
+
// 1003658). The onboarding copy lives in error.details, so reading it off the top
|
|
728
|
+
// level rendered an empty prompt even when the branch fired.
|
|
729
|
+
if (r.status === 403 && errCode(r.data) === 'stage_required') {
|
|
730
|
+
const d = errDetails(r.data);
|
|
727
731
|
console.log('');
|
|
728
|
-
console.log(
|
|
732
|
+
console.log(d.what_to_do || `Clear "${d.required_stage_label || d.required_stage}" before the work queue opens.`);
|
|
729
733
|
for (const line of plainCardLines('stage_required')) console.log(line);
|
|
730
734
|
console.log('');
|
|
731
735
|
process.exit(0);
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.970'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// tests/cli_err_code.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1003658 — ADR 0116 standardised every failure on a NESTED envelope:
|
|
4
|
+
//
|
|
5
|
+
// { "error": { "code": "task_not_found", "message": "…", "details": {…} } }
|
|
6
|
+
//
|
|
7
|
+
// so `r.data.error === 'task_not_found'` compares an OBJECT to a STRING and is never
|
|
8
|
+
// true. Each such comparison guarded a specific, helpful message, so against a live
|
|
9
|
+
// server every one of those branches was UNREACHABLE and the user fell through to a raw
|
|
10
|
+
// `(409): [object Object]` dump — a clear server refusal rendered as a tooling crash.
|
|
11
|
+
//
|
|
12
|
+
// NINE sites across SIX scripts now route through errCode/errDetails: abandon x3,
|
|
13
|
+
// release, demote x2, setup, start, login. (login.js was HALF converted — nested for
|
|
14
|
+
// idp_signing_unconfigured, flat for auth_not_configured — so one arm was dead. It was
|
|
15
|
+
// found by the source scan at the bottom of this file, not by reading.)
|
|
16
|
+
//
|
|
17
|
+
// THE HALF-FIX THIS ALSO GUARDS AGAINST. The sibling fields moved WITH the code.
|
|
18
|
+
// demote.js printed `r.data.current_status` beside its message; 0116 put that at
|
|
19
|
+
// `error.details.current_status`. Changing only the comparison restores the branch and
|
|
20
|
+
// then prints "it is 'undefined', not 'ready'" — which looks like a whole fix. Both
|
|
21
|
+
// readers are therefore tested, and both are tested against the REAL shapes observed
|
|
22
|
+
// live on 2026-09-22 rather than shapes we assumed.
|
|
23
|
+
//
|
|
24
|
+
// Run: node --test tests/cli_err_code.mjs
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const { errCode, errDetails } = require(path.join(REPO_ROOT, 'scripts/gds/cli-lib.js'));
|
|
35
|
+
|
|
36
|
+
// Captured verbatim from cloudbongos.com on 2026-09-22 — not invented.
|
|
37
|
+
const LIVE_ABANDON_SHIPPED = { error: { code: 'cannot_abandon_shipped', message: 'shipped tasks are immutable history' } };
|
|
38
|
+
const LIVE_DEMOTE_REFUSED = { error: { code: 'cannot_demote', message: 'cannot_demote', details: { current_status: 'shipped' } } };
|
|
39
|
+
const LIVE_NOT_FOUND = { error: { code: 'task_not_found', message: 'task_not_found' } };
|
|
40
|
+
|
|
41
|
+
test('THE BUG: the old flat comparison never matches a real envelope', () => {
|
|
42
|
+
// This is the whole finding. If this assertion ever fails, the envelope changed and
|
|
43
|
+
// the fix below is no longer needed — which is worth knowing loudly.
|
|
44
|
+
assert.notEqual(LIVE_ABANDON_SHIPPED.error, 'cannot_abandon_shipped');
|
|
45
|
+
assert.equal(typeof LIVE_ABANDON_SHIPPED.error, 'object',
|
|
46
|
+
'error is an OBJECT — comparing it to a string is why every branch was dead');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('errCode reads the slug out of real observed envelopes', () => {
|
|
50
|
+
assert.equal(errCode(LIVE_ABANDON_SHIPPED), 'cannot_abandon_shipped');
|
|
51
|
+
assert.equal(errCode(LIVE_DEMOTE_REFUSED), 'cannot_demote');
|
|
52
|
+
assert.equal(errCode(LIVE_NOT_FOUND), 'task_not_found');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('errCode still reads a legacy flat string', () => {
|
|
56
|
+
// Older routes, and any non-envelope transport error (a proxy returning a plain body,
|
|
57
|
+
// or a pre-0116 instance an upgraded CLI is talking to) still surface a bare string.
|
|
58
|
+
assert.equal(errCode({ error: 'auth_not_configured' }), 'auth_not_configured');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('errCode returns null rather than throwing on anything unexpected', () => {
|
|
62
|
+
for (const input of [null, undefined, {}, { error: null }, { error: {} }, { error: 42 }, { error: { code: 7 } }, 'a string']) {
|
|
63
|
+
assert.equal(errCode(input), null, `errCode(${JSON.stringify(input)}) must be null, never a throw`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('THE HALF-FIX GUARD: errDetails finds the sibling field 0116 moved', () => {
|
|
68
|
+
assert.equal(errDetails(LIVE_DEMOTE_REFUSED).current_status, 'shipped',
|
|
69
|
+
'reading current_status off the top level yields undefined — the half-fix this prevents');
|
|
70
|
+
assert.equal(LIVE_DEMOTE_REFUSED.current_status, undefined,
|
|
71
|
+
'proving the naive read really is empty, so the guard is not vacuous');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('errDetails falls back to the top-level body for a pre-0116 shape', () => {
|
|
75
|
+
assert.equal(errDetails({ error: 'cannot_demote', current_status: 'shipped' }).current_status, 'shipped');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('errDetails always returns an object, so callers can destructure safely', () => {
|
|
79
|
+
for (const input of [null, undefined, {}, { error: {} }, { error: { details: 'not-an-object' } }]) {
|
|
80
|
+
assert.equal(typeof errDetails(input), 'object');
|
|
81
|
+
assert.notEqual(errDetails(input), null);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('no flat `.error === <string>` comparison survives in scripts/gds', () => {
|
|
86
|
+
// The durable half: this is what stops a sixth script reintroducing the bug. It scans
|
|
87
|
+
// source rather than trusting that the sweep was complete — the filed row said 11
|
|
88
|
+
// sites across 6 scripts, a later correction said 4 across 2, and the truth was 8
|
|
89
|
+
// across 5. A count in prose rots; a scan does not.
|
|
90
|
+
const fs = require('node:fs');
|
|
91
|
+
const dir = path.join(REPO_ROOT, 'scripts/gds');
|
|
92
|
+
const offenders = [];
|
|
93
|
+
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.js'))) {
|
|
94
|
+
const src = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
95
|
+
src.split('\n').forEach((line, i) => {
|
|
96
|
+
if (/^\s*(\/\/|\*)/.test(line)) return; // commentary may quote the bug
|
|
97
|
+
if (/\.data\??\.error\s*===\s*['"]/.test(line)) offenders.push(`${f}:${i + 1}: ${line.trim()}`);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
assert.deepEqual(offenders, [],
|
|
101
|
+
'use errCode(r.data) — a flat comparison against the ADR 0116 envelope is always false');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// End-to-end branch selection (grader finding, round 1).
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// The tests above prove the PRIMITIVE is right. They do not prove each script WIRED it
|
|
108
|
+
// right — a script could call errCode() and still order its if/else wrongly, or read a
|
|
109
|
+
// sibling field off the top level. That second mistake is not hypothetical: the first
|
|
110
|
+
// round of this task shipped exactly it in release.js (`r.data.claimWorktree`, which
|
|
111
|
+
// ADR 0116 moved to error.details), and only the grader caught it.
|
|
112
|
+
//
|
|
113
|
+
// So this block asserts the WIRING, by the one property that distinguishes a real fix
|
|
114
|
+
// from a half fix: for every branch, no sibling field may be read off the top level of
|
|
115
|
+
// the response body. It scans source rather than spawning the CLIs, because spawning
|
|
116
|
+
// them needs a live server and a session — which would make this an integration test
|
|
117
|
+
// that silently self-skips, the failure mode tests/copy_inventory-class suites already
|
|
118
|
+
// demonstrate.
|
|
119
|
+
test('no converted branch reads a sibling payload field off the top level', () => {
|
|
120
|
+
const fs = require('node:fs');
|
|
121
|
+
// The fields ADR 0116 moved into error.details, and the scripts that render them.
|
|
122
|
+
const MOVED = ['claimWorktree', 'current_status', 'what_to_do', 'required_stage_label', 'required_stage'];
|
|
123
|
+
const offenders = [];
|
|
124
|
+
for (const f of ['abandon.js', 'release.js', 'demote.js', 'setup.js', 'start.js', 'login.js']) {
|
|
125
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts/gds', f), 'utf8');
|
|
126
|
+
src.split('\n').forEach((line, i) => {
|
|
127
|
+
if (/^\s*(\/\/|\*)/.test(line)) return;
|
|
128
|
+
for (const field of MOVED) {
|
|
129
|
+
if (new RegExp(`\\.data\\??\\.${field}\\b`).test(line)) offenders.push(`${f}:${i + 1}: ${line.trim()}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
assert.deepEqual(offenders, [],
|
|
134
|
+
'ADR 0116 moved these beside the code into error.details — read them via errDetails(r.data), '
|
|
135
|
+
+ 'or the branch fires and then renders undefined (the half-fix)');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('every script that compares an error code imports errCode', () => {
|
|
139
|
+
const fs = require('node:fs');
|
|
140
|
+
const missing = [];
|
|
141
|
+
for (const f of fs.readdirSync(path.join(REPO_ROOT, 'scripts/gds')).filter((n) => n.endsWith('.js'))) {
|
|
142
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'scripts/gds', f), 'utf8');
|
|
143
|
+
const uses = /\berrCode\s*\(/.test(src);
|
|
144
|
+
const imports = /require\('\.\/cli-lib'\)/.test(src) && /\berrCode\b/.test(src.split('\n').find((l) => l.includes("require('./cli-lib')")) || '');
|
|
145
|
+
if (uses && !imports && f !== 'cli-lib.js') missing.push(f);
|
|
146
|
+
}
|
|
147
|
+
assert.deepEqual(missing, [], 'a script calling errCode() without importing it throws at the branch it was meant to fix');
|
|
148
|
+
});
|
package/tests/plain_cards.mjs
CHANGED
|
@@ -181,7 +181,11 @@ test('the sign-in printers, the release refusals and the claim guard each print
|
|
|
181
181
|
|
|
182
182
|
test('every way start.js can refuse or fail prints a card', () => {
|
|
183
183
|
const src = read('scripts/gds/start.js');
|
|
184
|
-
|
|
184
|
+
// BOTH spellings, or this assertion pins the bug it was written above: ADR 0116
|
|
185
|
+
// nests the slug as `{ error: { code } }`, so the flat `.error === 'slug'` compares
|
|
186
|
+
// an object to a string and is never true. Task 1003658 moved the live site to
|
|
187
|
+
// errCode(); matching only the flat form made the FIX read as "the branch moved".
|
|
188
|
+
const codes = [...src.matchAll(/(?:\.error|errCode\([^)]*\)) === '([A-Za-z_]+)'/g)].map((m) => m[1]);
|
|
185
189
|
assert.ok(codes.includes('stage_required'), 'start.js no longer names its refusal — has the branch moved?');
|
|
186
190
|
assert.deepEqual(missing(codes), []);
|
|
187
191
|
for (const code of [...new Set(codes)]) assert.ok(src.includes(`plainCardLines('${code}')`), `start.js never prints the ${code} card`);
|
package/tests/provision.mjs
CHANGED
|
@@ -2858,6 +2858,111 @@ await ta('ensurePrivateRepoAccess(dry-run): fully OFFLINE — no token read, no
|
|
|
2858
2858
|
assert.equal(tokenReads, 0, 'not even the credential read');
|
|
2859
2859
|
});
|
|
2860
2860
|
|
|
2861
|
+
console.log('\nflip-to-private: wiring BEFORE the flip (task 1004167):');
|
|
2862
|
+
|
|
2863
|
+
// The flip must wire first and flip second — a deploy key authenticates fine against a
|
|
2864
|
+
// still-public repo, so wiring first leaves no broken window, whereas flipping first
|
|
2865
|
+
// breaks the very next deploy pull. But at wire time GitHub still answers `private:
|
|
2866
|
+
// false`, and the function branches on that OBSERVED value. `intendedPrivate` is the
|
|
2867
|
+
// caller's declaration of where the repo is going.
|
|
2868
|
+
|
|
2869
|
+
await ta('intendedPrivate: wires a repo GitHub still reports PUBLIC — the pre-flip window', async () => {
|
|
2870
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({ priv: false, existing: false });
|
|
2871
|
+
deps.intendedPrivate = true;
|
|
2872
|
+
const r = await P.ensurePrivateRepoAccess(privInst, deps);
|
|
2873
|
+
assert.equal(r.private, true, 'the declared INTENT decides, not the current visibility');
|
|
2874
|
+
const add = apiCalls.find((c) => c[0] === 'add');
|
|
2875
|
+
assert.ok(add, 'the key registers while the repo is still public — it authenticates either way');
|
|
2876
|
+
assert.equal(add[3], true, 'still read-only: the override widens WHEN we wire, never what the key may do');
|
|
2877
|
+
const joined = cmds.join('\n');
|
|
2878
|
+
assert.match(joined, /ssh-keygen -q -t ed25519/);
|
|
2879
|
+
assert.match(joined, /remote set-url origin git@github\.com:example-owner\/demo\.git/);
|
|
2880
|
+
assert.match(joined, /config core\.sshCommand/);
|
|
2881
|
+
});
|
|
2882
|
+
|
|
2883
|
+
await ta('intendedPrivate: does NOT un-wire an existing key — the un-wire branch is what made pre-flip wiring impossible', async () => {
|
|
2884
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({
|
|
2885
|
+
priv: false, existing: true,
|
|
2886
|
+
keys: [{ id: 11, title: 'cloudbongos-deploy-demo', key: 'ssh-ed25519 AAAAlocal', read_only: true }],
|
|
2887
|
+
});
|
|
2888
|
+
deps.intendedPrivate = true;
|
|
2889
|
+
const r = await P.ensurePrivateRepoAccess(privInst, deps);
|
|
2890
|
+
assert.ok(!r.unwired, 'the caller asked for wiring; tearing it down is the exact opposite');
|
|
2891
|
+
assert.ok(!apiCalls.some((c) => c[0] === 'del'), 'the registered key survives');
|
|
2892
|
+
const joined = cmds.join('\n');
|
|
2893
|
+
assert.ok(!/config --unset core\.sshCommand/.test(joined), 'sshCommand stays pinned');
|
|
2894
|
+
assert.ok(!/remote set-url origin git@github-deploy:/.test(joined), 'origin is not returned to the shared alias');
|
|
2895
|
+
});
|
|
2896
|
+
|
|
2897
|
+
await ta('intendedPrivate does NOT bypass the admin precondition', async () => {
|
|
2898
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({ priv: false, existing: false });
|
|
2899
|
+
deps.intendedPrivate = true;
|
|
2900
|
+
deps.githubRepos.getUserRepo = async () => ({ full_name: 'example-owner/demo', private: false, admin: false });
|
|
2901
|
+
await assert.rejects(() => P.ensurePrivateRepoAccess(privInst, deps), /repository access is not admin: .*ADMIN/);
|
|
2902
|
+
assert.ok(!cmds.some((c) => /ssh-keygen/.test(c)), 'no key minted for a credential that cannot register it');
|
|
2903
|
+
assert.ok(!apiCalls.some((c) => c[0] === 'add'));
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
await ta('no override → every existing path is byte-identical (the override is opt-in only)', async () => {
|
|
2907
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({ priv: false, existing: false });
|
|
2908
|
+
const r = await P.ensurePrivateRepoAccess(privInst, deps);
|
|
2909
|
+
assert.equal(r.private, false);
|
|
2910
|
+
assert.equal(cmds.length, 0);
|
|
2911
|
+
assert.deepEqual(apiCalls.map((c) => c[0]), ['get'], 'still one visibility read and nothing else');
|
|
2912
|
+
});
|
|
2913
|
+
|
|
2914
|
+
// A declared intent changes what a soft skip MEANS. Without it, "no credential" is a
|
|
2915
|
+
// warning about a repo that merely might be private. With it, the caller is about to
|
|
2916
|
+
// flip — so skipping quietly hands back an unwired checkout and the flip then breaks
|
|
2917
|
+
// the recurring pull for good (walk finding N4, the thing ADR 0176 exists to prevent).
|
|
2918
|
+
|
|
2919
|
+
await ta('intendedPrivate + no token: REFUSES rather than skipping — a silent skip would break the next pull', async () => {
|
|
2920
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({ token: null });
|
|
2921
|
+
deps.intendedPrivate = true;
|
|
2922
|
+
await assert.rejects(() => P.ensurePrivateRepoAccess(privInst, deps), /cannot be made private[\s\S]*credential/i);
|
|
2923
|
+
assert.equal(cmds.length, 0);
|
|
2924
|
+
assert.equal(apiCalls.length, 0);
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2927
|
+
await ta('intendedPrivate + unreachable GitHub: REFUSES rather than skipping', async () => {
|
|
2928
|
+
const { deps, cmds } = repoAuthDeps({ existing: false });
|
|
2929
|
+
deps.intendedPrivate = true;
|
|
2930
|
+
deps.githubRepos.getUserRepo = async () => { throw new Error('socket hang up'); };
|
|
2931
|
+
await assert.rejects(() => P.ensurePrivateRepoAccess(privInst, deps), /cannot be made private[\s\S]*try again/i);
|
|
2932
|
+
assert.ok(!cmds.some((c) => /ssh-keygen/.test(c)), 'nothing wired on a read we could not complete');
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
await ta('without the intent, both soft skips stay soft (unchanged for every existing caller)', async () => {
|
|
2936
|
+
const noTok = repoAuthDeps({ token: null });
|
|
2937
|
+
assert.equal((await P.ensurePrivateRepoAccess(privInst, noTok.deps)).skipped, 'no_token');
|
|
2938
|
+
const unreach = repoAuthDeps({ existing: false });
|
|
2939
|
+
unreach.deps.githubRepos.getUserRepo = async () => { throw new Error('socket hang up'); };
|
|
2940
|
+
assert.equal((await P.ensurePrivateRepoAccess(privInst, unreach.deps)).skipped, 'github_unreachable');
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
await ta('intendedPrivate: the apply-path log does not claim the repo IS private when GitHub says otherwise', async () => {
|
|
2944
|
+
const lines = [];
|
|
2945
|
+
const { deps } = repoAuthDeps({ priv: false, existing: false });
|
|
2946
|
+
deps.intendedPrivate = true;
|
|
2947
|
+
deps.log = (m) => lines.push(String(m));
|
|
2948
|
+
await P.ensurePrivateRepoAccess(privInst, deps);
|
|
2949
|
+
const joined = lines.join('\n');
|
|
2950
|
+
assert.doesNotMatch(joined, /is PRIVATE/, 'GitHub reported it public — the ops log must not misstate that');
|
|
2951
|
+
assert.match(joined, /ABOUT TO BE MADE PRIVATE/);
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
await ta('intendedPrivate(dry-run): names the pre-flip intent in the plan line, and stays OFFLINE', async () => {
|
|
2955
|
+
const lines = [];
|
|
2956
|
+
const { deps, cmds, apiCalls } = repoAuthDeps({ apply: false, priv: false, existing: false });
|
|
2957
|
+
deps.intendedPrivate = true;
|
|
2958
|
+
deps.log = (m) => lines.push(String(m));
|
|
2959
|
+
const r = await P.ensurePrivateRepoAccess(privInst, deps);
|
|
2960
|
+
assert.equal(r.planned, true);
|
|
2961
|
+
assert.equal(cmds.length, 0, 'no mutation in dry-run');
|
|
2962
|
+
assert.deepEqual(apiCalls, [], 'a plan run stays a safe inspection tool');
|
|
2963
|
+
assert.match(lines.join('\n'), /ABOUT TO BE MADE PRIVATE/, 'the plan says which intent it planned for');
|
|
2964
|
+
});
|
|
2965
|
+
|
|
2861
2966
|
await ta('provisionInstance(standalone): the private-repo wiring lands BETWEEN the scaffold and the step-2 pull', async () => {
|
|
2862
2967
|
const { deps, cmds } = repoAuthDeps({ existing: false });
|
|
2863
2968
|
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'example-owner/mercury' }, deps);
|