@bongos/core 1.21.123 → 1.21.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +25 -15
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/github-push.js +2 -1
- package/modules/lifecycle/local-land-proof.js +126 -0
- package/modules/lifecycle/publish-reconciler.js +15 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/local_land_proof.mjs +173 -0
- package/tests/publish_reconciler.mjs +3 -1
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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.124",
|
|
6
|
+
"core_contract": "1.21.124",
|
|
7
|
+
"source_commit": "695a6d5cad41a0d7a9e79cf58a7de3282b64daf3",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-14T19:
|
|
9
|
+
"built_at": "2026-10-14T19:49:05.130Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 605,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3124,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3758,
|
|
20
|
+
"tree_sha256": "97c9a1a6dc5c913bdacb02a6bbef3bef853e5babbbd971cd4f097403aa754be9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2922,7 +2922,7 @@
|
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "docs/module-api-changelog.md",
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
|
-
"sha256": "
|
|
2925
|
+
"sha256": "1ead6d6c8697d53d3f324f3d5c1523f5c590ebcce268abcb2b29a367d6c34f17"
|
|
2926
2926
|
},
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/modules-contract.md",
|
|
@@ -7242,7 +7242,7 @@
|
|
|
7242
7242
|
{
|
|
7243
7243
|
"path": "modules/lifecycle/github-push.js",
|
|
7244
7244
|
"mode": "0000644",
|
|
7245
|
-
"sha256": "
|
|
7245
|
+
"sha256": "b15eb0c36f0acb41d11f52393ca9696de55b9326a483de02624b3a4d5e87250c"
|
|
7246
7246
|
},
|
|
7247
7247
|
{
|
|
7248
7248
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -7349,6 +7349,11 @@
|
|
|
7349
7349
|
"mode": "0000644",
|
|
7350
7350
|
"sha256": "69e9bf495dfdbc324e7f5664e09a5ad30e6eac896c12656b7d6ecb38d6be81f9"
|
|
7351
7351
|
},
|
|
7352
|
+
{
|
|
7353
|
+
"path": "modules/lifecycle/local-land-proof.js",
|
|
7354
|
+
"mode": "0000644",
|
|
7355
|
+
"sha256": "7698b11de1978c29c1744032da5b28831a1f6823b905d60c8be8ac469916d46a"
|
|
7356
|
+
},
|
|
7352
7357
|
{
|
|
7353
7358
|
"path": "modules/lifecycle/matter-acts.js",
|
|
7354
7359
|
"mode": "0000644",
|
|
@@ -7507,7 +7512,7 @@
|
|
|
7507
7512
|
{
|
|
7508
7513
|
"path": "modules/lifecycle/publish-reconciler.js",
|
|
7509
7514
|
"mode": "0000644",
|
|
7510
|
-
"sha256": "
|
|
7515
|
+
"sha256": "d03c36a2b3e50bb2334db66d177514542185a7277873891ee6d3a99c5ad10464"
|
|
7511
7516
|
},
|
|
7512
7517
|
{
|
|
7513
7518
|
"path": "modules/lifecycle/removal-derivation.js",
|
|
@@ -10467,12 +10472,12 @@
|
|
|
10467
10472
|
{
|
|
10468
10473
|
"path": "package-lock.json",
|
|
10469
10474
|
"mode": "0000644",
|
|
10470
|
-
"sha256": "
|
|
10475
|
+
"sha256": "31a3c967bca985d1dc5007279f9ed7e9352b0466128af6edd9debbf4e7b592bb"
|
|
10471
10476
|
},
|
|
10472
10477
|
{
|
|
10473
10478
|
"path": "package.json",
|
|
10474
10479
|
"mode": "0000644",
|
|
10475
|
-
"sha256": "
|
|
10480
|
+
"sha256": "4714b7161307f36c4ae4322e72f3670f6d958b1ab3b19f728a3f05f880898771"
|
|
10476
10481
|
},
|
|
10477
10482
|
{
|
|
10478
10483
|
"path": "public-docs/index.html",
|
|
@@ -10492,7 +10497,7 @@
|
|
|
10492
10497
|
{
|
|
10493
10498
|
"path": "release-notes.json",
|
|
10494
10499
|
"mode": "0000644",
|
|
10495
|
-
"sha256": "
|
|
10500
|
+
"sha256": "aeb72646e01f775fe09e570e0f5e8dccf9733fc59cb79d5a67e6315f2195d88c"
|
|
10496
10501
|
},
|
|
10497
10502
|
{
|
|
10498
10503
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -12812,7 +12817,7 @@
|
|
|
12812
12817
|
{
|
|
12813
12818
|
"path": "src/module-api.js",
|
|
12814
12819
|
"mode": "0000644",
|
|
12815
|
-
"sha256": "
|
|
12820
|
+
"sha256": "792dad669f6c0d0b86433d206c837ca7ffcbf6a8b5b7c02bda8707c3dd3f7099"
|
|
12816
12821
|
},
|
|
12817
12822
|
{
|
|
12818
12823
|
"path": "src/module-loader/catalog.js",
|
|
@@ -16109,6 +16114,11 @@
|
|
|
16109
16114
|
"mode": "0000644",
|
|
16110
16115
|
"sha256": "64f8bd94dc461ca9b4ef82fae929d49675231afa6a24d6d65ba2dbe8721efc89"
|
|
16111
16116
|
},
|
|
16117
|
+
{
|
|
16118
|
+
"path": "tests/local_land_proof.mjs",
|
|
16119
|
+
"mode": "0000644",
|
|
16120
|
+
"sha256": "0a7825f1e35c7251c66ab986555b71728575d3dff8be95f8a017dd66190d186f"
|
|
16121
|
+
},
|
|
16112
16122
|
{
|
|
16113
16123
|
"path": "tests/local_preview.mjs",
|
|
16114
16124
|
"mode": "0000644",
|
|
@@ -17342,7 +17352,7 @@
|
|
|
17342
17352
|
{
|
|
17343
17353
|
"path": "tests/publish_reconciler.mjs",
|
|
17344
17354
|
"mode": "0000644",
|
|
17345
|
-
"sha256": "
|
|
17355
|
+
"sha256": "882dfd49bebd89bed3c2bb20d0bf78e5f7b4da45e525492e8742b43f9c4e19a9"
|
|
17346
17356
|
},
|
|
17347
17357
|
{
|
|
17348
17358
|
"path": "tests/publish_status_branch.mjs",
|
|
@@ -3083,5 +3083,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3083
3083
|
landed since 1.21.121 with no explicit bump. run 38620147259. (task 1002620)
|
|
3084
3084
|
1.21.123 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3085
3085
|
landed since 1.21.122 with no explicit bump. run 38624070870. (task 1002620)
|
|
3086
|
+
1.21.124 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3087
|
+
landed since 1.21.123 with no explicit bump. run 38626749089. (task 1002620)
|
|
3086
3088
|
---------------------------------------------------------------------------
|
|
3087
3089
|
```
|
|
@@ -1194,7 +1194,8 @@ async function landProvenForTask({ taskId, publishedSha, status }, deps = {}) {
|
|
|
1194
1194
|
async function isAncestorOfDefaultBranch({ sha, base = 'main' } = {}, deps = {}) {
|
|
1195
1195
|
if (typeof sha !== 'string' || !SAFE_SHA_RE.test(sha)) return false;
|
|
1196
1196
|
const token = await resolveToken(deps);
|
|
1197
|
-
|
|
1197
|
+
// task 1004633: no credential (a laptop-mode instance) → ask the instance's own checkout.
|
|
1198
|
+
if (!token) return (deps.localLandProof || require('./local-land-proof')).isOnOriginMain({ sha, base }, deps.local || {});
|
|
1198
1199
|
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
1199
1200
|
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
1200
1201
|
const { owner, repo } = repoInfo;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// local-land-proof — prove a land from the instance's OWN git checkout when the
|
|
4
|
+
// server holds no GitHub credential (task 1004633).
|
|
5
|
+
//
|
|
6
|
+
// WHY. Every land proof the lifecycle had asked GitHub: isAncestorOfDefaultBranch
|
|
7
|
+
// runs the compare API, and the publish reconciler returned early when
|
|
8
|
+
// isConfigured() was false. A laptop-mode instance (the owner's mercury project)
|
|
9
|
+
// merges a ship LOCALLY and pushes main itself — no PR, no server credential. So
|
|
10
|
+
// POST /tasks/:id/ship refused `ship_land_not_proven` (the compare threw
|
|
11
|
+
// PUSH_UNCONFIGURED, swallowed as "no proof"), the sweep never ran, and task 48
|
|
12
|
+
// sat at 'confirmed' with its merge commit on origin/main.
|
|
13
|
+
//
|
|
14
|
+
// THE PROOF is the same one the GitHub path makes, asked of a different witness:
|
|
15
|
+
// is the claim's recorded tip (claims.published_head_sha, written by attest-gate
|
|
16
|
+
// before its own credential check) an ancestor of origin/<base>? The witness is
|
|
17
|
+
// the remote-tracking ref after a fetch, never the local branch: a laptop ship
|
|
18
|
+
// merges into local main BEFORE it pushes, and a merge whose push failed must not
|
|
19
|
+
// count. A failed fetch (offline) falls back to the last fetched ref, which can
|
|
20
|
+
// only be behind the truth, so it can delay a ship but never invent one.
|
|
21
|
+
//
|
|
22
|
+
// Pure w.r.t. an injected `git` runner, so it unit-tests with no repository.
|
|
23
|
+
|
|
24
|
+
const { execFile } = require('node:child_process');
|
|
25
|
+
const { promisify } = require('node:util');
|
|
26
|
+
|
|
27
|
+
const execFileP = promisify(execFile);
|
|
28
|
+
|
|
29
|
+
// A real commit sha only: `merge-base` resolves refs, so "HEAD" or a branch name
|
|
30
|
+
// would answer about something other than the claim's tip.
|
|
31
|
+
const SAFE_SHA_RE = /^[0-9a-f]{7,64}$/;
|
|
32
|
+
const SAFE_BASE_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,100}$/;
|
|
33
|
+
|
|
34
|
+
function defaultGit(root) {
|
|
35
|
+
return (args) => execFileP('git', args, { cwd: root, timeout: 60_000, windowsHide: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveRoot(deps) {
|
|
39
|
+
if (deps.root) return deps.root;
|
|
40
|
+
return require('../../src/module-api').resolveInstanceRoot();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchOrigin(git, base) {
|
|
44
|
+
try {
|
|
45
|
+
await git(['fetch', '--quiet', 'origin', base]);
|
|
46
|
+
} catch (_) {
|
|
47
|
+
/* offline or no credential: the last fetched origin ref still answers, only later */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// true = the tip is in origin/<base>; false = it is not (or git does not know the
|
|
52
|
+
// commit). Throws LOCAL_PROOF_UNAVAILABLE when there is no usable checkout (git
|
|
53
|
+
// missing, not a repository) so a caller can tell "not landed" from "cannot say".
|
|
54
|
+
async function isOnOriginMain({ sha, base = 'main' } = {}, deps = {}) {
|
|
55
|
+
if (typeof sha !== 'string' || !SAFE_SHA_RE.test(sha)) return false;
|
|
56
|
+
if (typeof base !== 'string' || !SAFE_BASE_RE.test(base) || base.includes('..')) return false;
|
|
57
|
+
const git = deps.git || defaultGit(resolveRoot(deps));
|
|
58
|
+
if (!deps.fetched) await fetchOrigin(git, base);
|
|
59
|
+
try {
|
|
60
|
+
await git(['merge-base', '--is-ancestor', sha, `refs/remotes/origin/${base}`]);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// 1 = not an ancestor; 128 = an unknown commit (never reached origin).
|
|
64
|
+
if (e && (e.code === 1 || e.code === 128)) return false;
|
|
65
|
+
const out = new Error('LOCAL_PROOF_UNAVAILABLE');
|
|
66
|
+
out.code = 'LOCAL_PROOF_UNAVAILABLE';
|
|
67
|
+
out.cause = e;
|
|
68
|
+
throw out;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The reconciler's sweep for a host with no GitHub credential. Tip proof only: no
|
|
73
|
+
// PR, branch or deploy probe exists to consult, and a row with no recorded tip
|
|
74
|
+
// has nothing local to prove, so it waits (strand-watch reports it).
|
|
75
|
+
// One root and one fetch per sweep, not per row: origin/main is the same ref for
|
|
76
|
+
// every confirmed task the sweep reads.
|
|
77
|
+
async function sweepByLocalProof(rows, { db, emit, log = console.log, error = console.error, proof } = {}) {
|
|
78
|
+
if (!proof) {
|
|
79
|
+
let shared = null;
|
|
80
|
+
proof = async ({ sha }) => {
|
|
81
|
+
if (!shared) {
|
|
82
|
+
shared = { git: defaultGit(resolveRoot({})), fetched: true };
|
|
83
|
+
await fetchOrigin(shared.git, 'main');
|
|
84
|
+
}
|
|
85
|
+
return isOnOriginMain({ sha }, shared);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
let checked = 0;
|
|
89
|
+
let reconciled = 0;
|
|
90
|
+
let errors = 0;
|
|
91
|
+
for (const r of rows || []) {
|
|
92
|
+
const tip = r && r.published_head_sha;
|
|
93
|
+
if (!tip || !SAFE_SHA_RE.test(String(tip))) continue;
|
|
94
|
+
checked++;
|
|
95
|
+
let landed = false;
|
|
96
|
+
try {
|
|
97
|
+
landed = await proof({ sha: String(tip) });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
errors++;
|
|
100
|
+
error(`[publish-reconciler] task ${r.task_id}: local land proof unavailable: ${e && e.message}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!landed) continue;
|
|
104
|
+
const reason = `published tip ${String(tip).slice(0, 8)} in origin/main (local checkout; no GitHub credential)`;
|
|
105
|
+
try {
|
|
106
|
+
const shipped = await db.shipTask({
|
|
107
|
+
taskId: r.task_id,
|
|
108
|
+
builderId: r.builder_id,
|
|
109
|
+
landAssurance: { kind: 'proof', reason },
|
|
110
|
+
});
|
|
111
|
+
reconciled++;
|
|
112
|
+
log(`[publish-reconciler] task ${r.task_id} (${reason}) landed → shipped`);
|
|
113
|
+
try { await emit('task.shipped', shipped); } catch (e) {
|
|
114
|
+
error(`[publish-reconciler] ship broadcast for task ${r.task_id} (non-blocking): ${e && e.message}`);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (!e || e.code !== 'TASK_NOT_CONFIRMED') {
|
|
118
|
+
errors++;
|
|
119
|
+
error(`[publish-reconciler] shipTask ${r.task_id} failed: ${e && e.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { checked, reconciled, errors };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { isOnOriginMain, sweepByLocalProof, SAFE_SHA_RE };
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
const githubPush = require('./github-push');
|
|
77
77
|
const seams = require('../../src/module-api');
|
|
78
78
|
const conflictResolve = require('./conflict-resolve');
|
|
79
|
+
const localLandProof = require('./local-land-proof'); // task 1004633
|
|
79
80
|
// The claim gate's grace for a strand its builder may be unable to clear — imported
|
|
80
81
|
// rather than restated so the sweep's escalation and the gate agree on one number
|
|
81
82
|
// (task 1003294).
|
|
@@ -195,10 +196,22 @@ async function runSweep(deps = {}) {
|
|
|
195
196
|
if (!warn.unconfigured) {
|
|
196
197
|
warn.unconfigured = true;
|
|
197
198
|
errFn(
|
|
198
|
-
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} —
|
|
199
|
+
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — no GitHub credential (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY — installation id is auto-discovered — or GITHUB_PUSH_TOKEN): confirmed→shipped lands only on a recorded tip this instance's own checkout finds in origin/main. Subsequent sweeps stay quiet until this changes.`
|
|
199
200
|
);
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
+
// task 1004633: a laptop-mode instance merges and pushes main itself, with no
|
|
203
|
+
// server credential. Returning here stranded its merged tasks at 'confirmed'
|
|
204
|
+
// forever (mercury task 48), so prove the recorded tip from the local checkout.
|
|
205
|
+
const local = deps.localLandProof || localLandProof;
|
|
206
|
+
let rows = [];
|
|
207
|
+
try {
|
|
208
|
+
rows = await db.listConfirmedTasksForReconcile({ limit: deps.limit || SWEEP_LIMIT });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
errFn(`[publish-reconciler] could not list confirmed tasks: ${e && e.message}`);
|
|
211
|
+
return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 1, strandsFlagged: 0 };
|
|
212
|
+
}
|
|
213
|
+
const swept = await local.sweepByLocalProof(rows, { db, emit, log: logFn, error: errFn });
|
|
214
|
+
return { skipped: 'unconfigured', checked: swept.checked, reconciled: swept.reconciled, merged: 0, resolvedConflicts: 0, errors: swept.errors, strandsFlagged: 0 };
|
|
202
215
|
}
|
|
203
216
|
warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
|
|
204
217
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.124",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.124",
|
|
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.21.
|
|
3
|
+
"version": "1.21.124",
|
|
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/release-notes.json
CHANGED
|
@@ -9720,5 +9720,11 @@
|
|
|
9720
9720
|
"id": "1004631",
|
|
9721
9721
|
"text": "Found why the AI refused to ship on Windows projects: the core update failed on Windows, so projects never got the permission that lets the AI run a ship. That is fixed. New projects also tell the AI to ship on its own in Auto"
|
|
9722
9722
|
}
|
|
9723
|
+
],
|
|
9724
|
+
"1.21.124": [
|
|
9725
|
+
{
|
|
9726
|
+
"id": "1004633",
|
|
9727
|
+
"text": "Found why a merged task on your own project stayed at 'confirmed': with no GitHub key set up on the project's server, Bongos could not prove the merge happened, and its background checker switched itself off. It now confirms t"
|
|
9728
|
+
}
|
|
9723
9729
|
]
|
|
9724
9730
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.124'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// tests/local_land_proof.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004633 — a merged task on an instance with no GitHub credential becomes
|
|
4
|
+
// 'shipped'. The owner's mercury project merged task 48 locally and pushed main;
|
|
5
|
+
// with no server credential the ship route refused (the compare API threw
|
|
6
|
+
// PUSH_UNCONFIGURED) and the reconciler returned early, so the task sat at
|
|
7
|
+
// 'confirmed' with its merge on origin/main. These pin the three halves of the fix:
|
|
8
|
+
// 1. isOnOriginMain proves a tip against a REAL repository's origin ref, and a
|
|
9
|
+
// merge that never reached origin does not count;
|
|
10
|
+
// 2. landProvenForTask falls back to it when there is no token;
|
|
11
|
+
// 3. the unconfigured reconciler sweep ships a proven row, and only that row.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/local_land_proof.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const local = require('../modules/lifecycle/local-land-proof.js');
|
|
24
|
+
const gh = require('../modules/lifecycle/github-push.js');
|
|
25
|
+
const reconciler = require('../modules/lifecycle/publish-reconciler.js');
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
async function test(name, fn) {
|
|
30
|
+
try {
|
|
31
|
+
await fn();
|
|
32
|
+
passed++;
|
|
33
|
+
console.log(` ok ${name}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
failed++;
|
|
36
|
+
console.log(` FAIL ${name}\n ${err && err.stack}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
41
|
+
|
|
42
|
+
// A bare origin plus a clone, the shape of a laptop-mode instance checkout.
|
|
43
|
+
function mkRepo() {
|
|
44
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'local-land-proof-'));
|
|
45
|
+
const origin = path.join(dir, 'origin.git');
|
|
46
|
+
const work = path.join(dir, 'work');
|
|
47
|
+
git(dir, 'init', '--quiet', '--bare', '-b', 'main', origin);
|
|
48
|
+
git(dir, 'clone', '--quiet', origin, work);
|
|
49
|
+
git(work, 'config', 'user.email', 't@example.com');
|
|
50
|
+
git(work, 'config', 'user.name', 't');
|
|
51
|
+
git(work, 'checkout', '--quiet', '-b', 'main');
|
|
52
|
+
fs.writeFileSync(path.join(work, 'a.txt'), 'a\n');
|
|
53
|
+
git(work, 'add', 'a.txt');
|
|
54
|
+
git(work, 'commit', '--quiet', '-m', 'base');
|
|
55
|
+
git(work, 'push', '--quiet', 'origin', 'main');
|
|
56
|
+
return { dir, work };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function commit(work, file) {
|
|
60
|
+
fs.writeFileSync(path.join(work, file), `${file}\n`);
|
|
61
|
+
git(work, 'add', file);
|
|
62
|
+
git(work, 'commit', '--quiet', '-m', file);
|
|
63
|
+
return git(work, 'rev-parse', 'HEAD');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await test('a tip merged and pushed to origin/main is proven', async () => {
|
|
67
|
+
const { dir, work } = mkRepo();
|
|
68
|
+
try {
|
|
69
|
+
git(work, 'checkout', '--quiet', '-b', 'task-48');
|
|
70
|
+
const tip = commit(work, 'b.txt');
|
|
71
|
+
git(work, 'checkout', '--quiet', 'main');
|
|
72
|
+
git(work, 'merge', '--quiet', '--no-ff', '-m', 'Merge task-48: (no value summary)', 'task-48');
|
|
73
|
+
git(work, 'push', '--quiet', 'origin', 'main');
|
|
74
|
+
assert.equal(await local.isOnOriginMain({ sha: tip }, { root: work }), true);
|
|
75
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await test('a merge into local main that never reached origin is NOT proven', async () => {
|
|
79
|
+
const { dir, work } = mkRepo();
|
|
80
|
+
try {
|
|
81
|
+
const tip = commit(work, 'c.txt'); // on local main, unpushed
|
|
82
|
+
assert.equal(await local.isOnOriginMain({ sha: tip }, { root: work }), false);
|
|
83
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('an unknown commit or a ref-shaped value is not proven', async () => {
|
|
87
|
+
const { dir, work } = mkRepo();
|
|
88
|
+
try {
|
|
89
|
+
assert.equal(await local.isOnOriginMain({ sha: 'c'.repeat(40) }, { root: work }), false);
|
|
90
|
+
assert.equal(await local.isOnOriginMain({ sha: 'HEAD' }, { root: work }), false, 'a ref must never resolve');
|
|
91
|
+
assert.equal(await local.isOnOriginMain({ sha: 'a'.repeat(40), base: '../x' }, { root: work }), false);
|
|
92
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await test('no usable checkout throws LOCAL_PROOF_UNAVAILABLE, never a silent false', async () => {
|
|
96
|
+
const broken = async () => { const e = new Error('spawn git ENOENT'); e.code = 'ENOENT'; throw e; };
|
|
97
|
+
await assert.rejects(
|
|
98
|
+
local.isOnOriginMain({ sha: 'a'.repeat(40) }, { git: broken }),
|
|
99
|
+
(e) => e.code === 'LOCAL_PROOF_UNAVAILABLE'
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('a caller that already fetched (the sweep) is not made to fetch again per row', async () => {
|
|
104
|
+
const calls = [];
|
|
105
|
+
const fake = async (args) => { calls.push(args[0]); };
|
|
106
|
+
await local.isOnOriginMain({ sha: 'a'.repeat(40) }, { git: fake });
|
|
107
|
+
await local.isOnOriginMain({ sha: 'b'.repeat(40) }, { git: fake, fetched: true });
|
|
108
|
+
await local.isOnOriginMain({ sha: 'c'.repeat(40) }, { git: fake, fetched: true });
|
|
109
|
+
assert.deepEqual(calls, ['fetch', 'merge-base', 'merge-base', 'merge-base']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await test('landProvenForTask with no credential proves the tip from the local checkout', async () => {
|
|
113
|
+
const saved = { t: process.env.GITHUB_PUSH_TOKEN, a: process.env.GITHUB_APP_ID, k: process.env.GITHUB_APP_PRIVATE_KEY };
|
|
114
|
+
delete process.env.GITHUB_PUSH_TOKEN; delete process.env.GITHUB_APP_ID; delete process.env.GITHUB_APP_PRIVATE_KEY;
|
|
115
|
+
try {
|
|
116
|
+
const asked = [];
|
|
117
|
+
const localLandProof = { isOnOriginMain: async ({ sha }) => { asked.push(sha); return sha === 'a'.repeat(40); } };
|
|
118
|
+
const yes = await gh.landProvenForTask({ taskId: 48, publishedSha: 'a'.repeat(40), status: null }, { localLandProof });
|
|
119
|
+
assert.equal(yes.proven, true);
|
|
120
|
+
const no = await gh.landProvenForTask({ taskId: 49, publishedSha: 'b'.repeat(40), status: null }, { localLandProof });
|
|
121
|
+
assert.equal(no.proven, false);
|
|
122
|
+
assert.deepEqual(asked, ['a'.repeat(40), 'b'.repeat(40)]);
|
|
123
|
+
} finally {
|
|
124
|
+
for (const [k, v] of [['GITHUB_PUSH_TOKEN', saved.t], ['GITHUB_APP_ID', saved.a], ['GITHUB_APP_PRIVATE_KEY', saved.k]]) {
|
|
125
|
+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await test('the unconfigured sweep ships a proven tip — and only that row', async () => {
|
|
131
|
+
const PROVEN = 'a'.repeat(40);
|
|
132
|
+
const shipped = [];
|
|
133
|
+
const events = [];
|
|
134
|
+
const rows = [
|
|
135
|
+
{ task_id: 48, builder_id: 7, worktree_name: 'task-48', published_head_sha: PROVEN },
|
|
136
|
+
{ task_id: 49, builder_id: 7, worktree_name: 'task-49', published_head_sha: 'b'.repeat(40) },
|
|
137
|
+
{ task_id: 50, builder_id: 7, worktree_name: 'task-50', published_head_sha: null },
|
|
138
|
+
];
|
|
139
|
+
const r = await reconciler.runSweep({
|
|
140
|
+
db: {
|
|
141
|
+
listConfirmedTasksForReconcile: async () => rows,
|
|
142
|
+
shipTask: async (a) => { shipped.push(a); return { id: a.taskId }; },
|
|
143
|
+
},
|
|
144
|
+
githubPush: { isConfigured: () => false },
|
|
145
|
+
localLandProof: {
|
|
146
|
+
sweepByLocalProof: (rs, o) => local.sweepByLocalProof(rs, { ...o, proof: async ({ sha }) => sha === PROVEN }),
|
|
147
|
+
},
|
|
148
|
+
emit: async (name, t) => { events.push([name, t.id]); },
|
|
149
|
+
log: () => {},
|
|
150
|
+
error: () => {},
|
|
151
|
+
});
|
|
152
|
+
assert.equal(r.skipped, 'unconfigured');
|
|
153
|
+
assert.equal(r.reconciled, 1);
|
|
154
|
+
assert.equal(r.checked, 2, 'the row with no tip has nothing local to prove');
|
|
155
|
+
assert.deepEqual(shipped.map((s) => s.taskId), [48]);
|
|
156
|
+
assert.equal(shipped[0].landAssurance.kind, 'proof');
|
|
157
|
+
assert.deepEqual(events, [['task.shipped', 48]]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await test('a sweep racing another flip is not an error (TASK_NOT_CONFIRMED)', async () => {
|
|
161
|
+
const r = await local.sweepByLocalProof(
|
|
162
|
+
[{ task_id: 48, builder_id: 7, published_head_sha: 'a'.repeat(40) }],
|
|
163
|
+
{
|
|
164
|
+
db: { shipTask: async () => { const e = new Error('x'); e.code = 'TASK_NOT_CONFIRMED'; throw e; } },
|
|
165
|
+
emit: async () => {}, log: () => {}, error: () => {},
|
|
166
|
+
proof: async () => true,
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
assert.deepEqual(r, { checked: 1, reconciled: 0, errors: 0 });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
173
|
+
if (failed) process.exit(1);
|
|
@@ -205,7 +205,9 @@ function mkDeps({
|
|
|
205
205
|
|
|
206
206
|
// ---------------------------------------------------------------------------
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
// task 1004633: an unconfigured sweep now reads the confirmed rows (to prove a recorded tip
|
|
209
|
+
// from the local checkout), but a row with no tip still ships nothing and GitHub is never asked.
|
|
210
|
+
test('no push credential → skipped: unconfigured; a row with no tip ships nothing, GitHub never called', async () => {
|
|
209
211
|
const deps = mkDeps({ configured: false, rows: [{ task_id: 1, builder_id: 9, worktree_name: 'wt' }] });
|
|
210
212
|
const r = await reconciler.runSweep(deps);
|
|
211
213
|
assert.equal(r.skipped, 'unconfigured');
|