@bongos/core 1.19.1038 → 1.19.1040
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 +98 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +39 -1
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +2 -2
- package/docs/copy-registry.json +2 -2
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/core-release-pipeline.md +1 -1
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/modules/hall-ui/public/deploy.html +9 -0
- package/modules/hall-ui/public/deploy.js +14 -1
- package/modules/hall-ui/public/deploy.states.json +14 -0
- package/modules/lifecycle/conflict-resolve.js +9 -1
- package/modules/npm-release/module.json +19 -0
- package/modules/npm-release/public/work.css +8 -0
- package/modules/npm-release/public/work.js +261 -0
- package/modules/npm-release/registry.js +271 -0
- package/modules/npm-release/routes/work.js +46 -0
- package/modules/npm-release/work.js +252 -0
- package/modules/provisioning/starter-bundles.js +6 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/gds/exec-path-guard.js +1 -1
- package/scripts/gds/install-git-hooks.js +18 -2
- package/scripts/gds/plain-cards.js +5 -0
- package/scripts/gds/publish-integration-gate.js +142 -0
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/ship-land.js +72 -6
- package/src/bongos/module-scope-map.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +15 -1
- package/tests/merge_driver_register.mjs +28 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_page.mjs +173 -0
- package/tests/npm_release_work.mjs +350 -0
- package/tests/publish_integration_gate.mjs +198 -0
- package/tests/render_check.mjs +3 -2
- package/tests/ship_land_ci_not_started.mjs +168 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// tests/ship_land_ci_not_started.mjs — a land wait that says "CI never ran" when it did
|
|
2
|
+
// not, instead of "fix your code" (task 1004289).
|
|
3
|
+
//
|
|
4
|
+
// WHY. During the 2026-09-25 GitHub Actions budget outage (blocker 1000173) every job on
|
|
5
|
+
// every PR "failed" in about two seconds without running: conclusion FAILURE, no steps, no
|
|
6
|
+
// runner, and one annotation — "The job was not started because an Actions budget is
|
|
7
|
+
// preventing further use." Task 1003764's red-check reader saw an ordinary red `unit`
|
|
8
|
+
// check and told the builder on PR 1211 to fix it, commit and re-run, plus the "CI tests
|
|
9
|
+
// your branch MERGED with main" trap. None of that can help: nothing was tested, and the
|
|
10
|
+
// fix is the owner's, on the GitHub account. These drive the laptop poll with a fake gh
|
|
11
|
+
// carrying the REAL payloads from PR 1211 (run 36210373664, attempt 1).
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/ship_land_ci_not_started.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import {
|
|
18
|
+
jobNeverStarted, landPendingLines, localMergeDriver, notStartedLines, pollCiDeploy, redBlocksMerge, redPrLine, redPrLines,
|
|
19
|
+
} from '../scripts/gds/ship-land.js';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { CARDS, cardFor } = require('../scripts/gds/plain-cards.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
async function test(name, fn) {
|
|
27
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
async function capture(fn) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
const orig = console.log;
|
|
33
|
+
console.log = (...a) => { lines.push(a.join(' ')); };
|
|
34
|
+
try { return { result: await fn(), out: lines.join('\n') }; }
|
|
35
|
+
finally { console.log = orig; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SHA = 'b9b03b03687ce823c016a29b0141736a3595c547';
|
|
39
|
+
const RUN = '36210373664';
|
|
40
|
+
const JOB_UNIT = '108315429264';
|
|
41
|
+
const JOB_REPORT = '108315436139';
|
|
42
|
+
const jobUrl = (job) => `https://github.com/example-org/example-repo/actions/runs/${RUN}/job/${job}`;
|
|
43
|
+
const checkRun = (name, job, conclusion = 'FAILURE') => ({
|
|
44
|
+
__typename: 'CheckRun', name, conclusion, status: 'COMPLETED', workflowName: 'unit', detailsUrl: jobUrl(job),
|
|
45
|
+
startedAt: '2026-09-26T02:01:46Z', completedAt: '2026-09-26T02:01:48Z',
|
|
46
|
+
});
|
|
47
|
+
const ctx = (context, state) => ({ __typename: 'StatusContext', context, state, targetUrl: '' });
|
|
48
|
+
const openPr = (rollup) => ({ state: 'OPEN', mergeCommit: null, mergeStateStatus: 'BLOCKED', headRefOid: SHA, statusCheckRollup: rollup });
|
|
49
|
+
// PR 1211's rollup during the outage: both unit-workflow jobs "failed", the trust status green.
|
|
50
|
+
const OUTAGE_ROLLUP = [checkRun('unit', JOB_UNIT), checkRun('report', JOB_REPORT), ctx('gate-author-trust', 'SUCCESS')];
|
|
51
|
+
|
|
52
|
+
// The two annotations GitHub returned for job 108315429264, verbatim (paths trimmed).
|
|
53
|
+
const BUDGET_NOTES = [
|
|
54
|
+
{ path: '.github', annotation_level: 'failure', title: '', message: 'The job was not started because an Actions budget is preventing further use.' },
|
|
55
|
+
{ path: '.github', annotation_level: 'notice', title: '', message: '"The ubuntu-latest label will migrate to Ubuntu 26 beginning October 19, 2026."' },
|
|
56
|
+
];
|
|
57
|
+
const NOTICE_ONLY = [BUDGET_NOTES[1]];
|
|
58
|
+
const annotationsPath = (job) => `repos/{owner}/{repo}/check-runs/${job}/annotations`;
|
|
59
|
+
|
|
60
|
+
// ---- the detector ----
|
|
61
|
+
|
|
62
|
+
await test('jobNeverStarted: the budget annotation on the job reads as never started', async () => {
|
|
63
|
+
const asked = [];
|
|
64
|
+
const gh = (args) => { asked.push(args); return BUDGET_NOTES; };
|
|
65
|
+
assert.equal(jobNeverStarted(checkRun('unit', JOB_UNIT), gh), true);
|
|
66
|
+
assert.deepEqual(asked, [['api', annotationsPath(JOB_UNIT)]], 'reads the check run named by the job id in its detailsUrl');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await test('jobNeverStarted: the payment-failed wording is the same shape and is caught too', async () => {
|
|
70
|
+
const gh = () => [{ annotation_level: 'failure', message: 'The job was not started because recent account payments have failed or your spending limit needs to be increased.' }];
|
|
71
|
+
assert.equal(jobNeverStarted(checkRun('unit', JOB_UNIT), gh), true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await test('jobNeverStarted is FAIL-OPEN: a real failure, an unreadable read, or no job id is "no"', async () => {
|
|
75
|
+
assert.equal(jobNeverStarted(checkRun('unit', JOB_UNIT), () => NOTICE_ONLY), false, 'a job that ran and failed');
|
|
76
|
+
assert.equal(jobNeverStarted(checkRun('unit', JOB_UNIT), () => null), false, 'gh could not read the annotations');
|
|
77
|
+
assert.equal(jobNeverStarted(checkRun('unit', JOB_UNIT), () => ({ message: 'Not Found' })), false, 'an error object, not a list');
|
|
78
|
+
let calls = 0;
|
|
79
|
+
const gh = () => { calls++; return BUDGET_NOTES; };
|
|
80
|
+
assert.equal(jobNeverStarted({ __typename: 'CheckRun', name: 'x', detailsUrl: 'https://example.com/checks' }, gh), false);
|
|
81
|
+
assert.equal(jobNeverStarted(ctx('unit-report', 'FAILURE'), gh), false, 'a commit status is not a job');
|
|
82
|
+
assert.equal(calls, 0, 'nothing to look up, so nothing is asked');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- the merge-driver answer and the lines it prints ----
|
|
86
|
+
|
|
87
|
+
await test('localMergeDriver: every red check never started → ci_not_started, not a failing check', async () => {
|
|
88
|
+
const md = localMergeDriver(openPr(OUTAGE_ROLLUP), () => 'must not be asked', () => true);
|
|
89
|
+
assert.deepEqual(md, { reason: 'ci_not_started:unit', not_started: true, failing_tests: null, run_id: RUN });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('localMergeDriver: one real failure beside a never-started job is still a failing check', async () => {
|
|
93
|
+
const rollup = [checkRun('unit', JOB_UNIT), checkRun('lint', '999', 'FAILURE')];
|
|
94
|
+
const md = localMergeDriver(openPr(rollup), () => null, (c) => c.name === 'unit');
|
|
95
|
+
assert.equal(md.reason, 'failing_check:unit');
|
|
96
|
+
assert.equal(md.not_started, undefined);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await test('localMergeDriver: without a never-started answer (the default) nothing changes', async () => {
|
|
100
|
+
assert.deepEqual(localMergeDriver(openPr(OUTAGE_ROLLUP), () => null), { reason: 'failing_check:unit', failing_tests: null });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('the red line says CI never ran and the work needs nothing; no fix-your-code, no merge trap', async () => {
|
|
104
|
+
const md = { reason: 'ci_not_started:unit', not_started: true, failing_tests: null, run_id: RUN };
|
|
105
|
+
const out = redPrLines(md, { taskId: 42, via: 'local' }).join('\n');
|
|
106
|
+
assert.match(redPrLine(md), /CI NEVER RAN/);
|
|
107
|
+
assert.match(out, /the `unit` job/);
|
|
108
|
+
assert.match(out, /nothing in it to fix/);
|
|
109
|
+
assert.match(out, /repo OWNER's to fix/);
|
|
110
|
+
assert.match(out, new RegExp(`gh run rerun ${RUN} --failed`), 'names the re-run for the exact run');
|
|
111
|
+
assert.match(out, /do not re-push or edit the work/);
|
|
112
|
+
assert.doesNotMatch(out, /fix it, commit/, 'the advice that cannot help is gone');
|
|
113
|
+
assert.doesNotMatch(out, /MERGED with the current main/, 'and so is the merge trap');
|
|
114
|
+
assert.ok(out.includes(CARDS.ci_not_started.happened), 'the plain card rides along');
|
|
115
|
+
assert.ok(!out.includes(CARDS.failing_check.happened), 'not the "a test failed" card');
|
|
116
|
+
assert.equal(cardFor('ci_not_started:unit'), CARDS.ci_not_started, 'the named reason resolves to its card');
|
|
117
|
+
assert.equal(redBlocksMerge(md), false, 'it is not the failing-check kind of red');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await test('notStartedLines without a run id points at the PR button instead of inventing a command', async () => {
|
|
121
|
+
const out = notStartedLines({ reason: 'ci_not_started:unit', not_started: true, run_id: null }).join('\n');
|
|
122
|
+
assert.match(out, /"Re-run failed jobs" on the PR/);
|
|
123
|
+
assert.doesNotMatch(out, /gh run rerun/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await test('the closing banner for a never-started CI is neither "NOT a failure" nor "fix the check"', async () => {
|
|
127
|
+
const out = landPendingLines(42, { notStarted: true }).join('\n');
|
|
128
|
+
assert.match(out, /CI never started/);
|
|
129
|
+
assert.match(out, /Nothing in the work needs to change/);
|
|
130
|
+
assert.match(out, /node scripts\/gds\/ship\.js 42/);
|
|
131
|
+
assert.doesNotMatch(out, /NOT a failure/);
|
|
132
|
+
assert.doesNotMatch(out, /Do what the lines above say/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---- the laptop poll, end to end ----
|
|
136
|
+
|
|
137
|
+
await test('pollCiDeploy (laptop): an Actions-budget outage is named as CI never running, once, with no code advice', async () => {
|
|
138
|
+
const seen = { prView: 0, annotations: 0, status: 0 };
|
|
139
|
+
const gh = (args) => {
|
|
140
|
+
if (args[0] === 'pr' && args[1] === 'view') { seen.prView++; return openPr(OUTAGE_ROLLUP); }
|
|
141
|
+
if (args[0] === 'api' && /check-runs\/\d+\/annotations$/.test(args[1])) { seen.annotations++; return BUDGET_NOTES; }
|
|
142
|
+
if (args[0] === 'api') { seen.status++; return { statuses: [] }; }
|
|
143
|
+
throw new Error(`unexpected gh call: ${args.join(' ')}`);
|
|
144
|
+
};
|
|
145
|
+
const { result, out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
|
|
146
|
+
assert.equal(result.pending, true, 'the land is still pending — the checks can yet run');
|
|
147
|
+
assert.equal(seen.prView, 30, 'polled the whole window');
|
|
148
|
+
assert.equal(seen.annotations, 2, 'each job\'s annotations are read once, not once per poll');
|
|
149
|
+
assert.equal(out.split('CI NEVER RAN on this PR').length - 1, 1, 'named once');
|
|
150
|
+
assert.match(out, /CI never started on the PR/, 'the closing banner says the same');
|
|
151
|
+
assert.doesNotMatch(out, /fix it, commit/);
|
|
152
|
+
assert.doesNotMatch(out, /MERGED with the current main/);
|
|
153
|
+
assert.doesNotMatch(out, /This is NOT a failure/);
|
|
154
|
+
assert.ok(!out.includes(CARDS.land_pending.happened), 'nor the "nothing to do" card');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await test('pollCiDeploy: when the annotations cannot be read, it falls back to the ordinary red wording', async () => {
|
|
158
|
+
const gh = (args) => {
|
|
159
|
+
if (args[0] === 'pr' && args[1] === 'view') return openPr(OUTAGE_ROLLUP);
|
|
160
|
+
return null; // no Checks read on this gh, or the API is down
|
|
161
|
+
};
|
|
162
|
+
const { out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
|
|
163
|
+
assert.match(out, /the PR is RED — the `unit` check failed/);
|
|
164
|
+
assert.doesNotMatch(out, /CI NEVER RAN/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
console.log(`\nship-land CI never started (task 1004289) tests: ${passed} passed, ${failed} failed`);
|
|
168
|
+
process.exitCode = failed > 0 ? 1 : 0;
|