@bongos/core 1.21.123 → 1.21.125
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 +46 -16
- package/bin/bongos.js +9 -0
- package/docs/module-api-changelog.md +4 -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/modules/onboarding/howto-guide.js +259 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/core-pin-sync.js +233 -0
- package/src/module-api.js +1 -1
- package/tests/core_pin_sync.mjs +237 -0
- package/tests/howto_guide.mjs +96 -0
- package/tests/local_land_proof.mjs +173 -0
- package/tests/publish_reconciler.mjs +3 -1
|
@@ -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');
|