Merge release-gate diagnostic correction from PR #3195

Source-PR: https://github.com/affaan-m/ECC/pull/3195
Source-Head: ffb69744b0
This commit is contained in:
affaan-m
2026-09-27 23:48:05 -04:00
2 changed files with 179 additions and 88 deletions
+42 -28
View File
@@ -79,6 +79,8 @@ function setting(value, fallback, maximum) {
return parsed;
}
class ReleaseGateDeadlineError extends Error {}
function createGithubClient(inputs, fetchImpl = fetch, options = {}) {
validateInputs(inputs);
const now = options.now || (() => performance.now());
@@ -88,7 +90,7 @@ function createGithubClient(inputs, fetchImpl = fetch, options = {}) {
function remaining() {
const left = deadline - now();
if (left <= 0) throw new Error('Release gate global deadline exceeded');
if (left <= 0) throw new ReleaseGateDeadlineError('Release gate global deadline exceeded');
return left;
}
@@ -101,7 +103,7 @@ function createGithubClient(inputs, fetchImpl = fetch, options = {}) {
new Promise((_, reject) => {
timer = setTimeout(() => {
controller.abort();
reject(new Error('Release gate request or global deadline exceeded'));
reject(new ReleaseGateDeadlineError('Release gate request or global deadline exceeded'));
}, Math.min(limit, remaining()));
}),
]);
@@ -253,8 +255,9 @@ function selectRuns(runs, inputs, trusted) {
return { ci: select(trusted.ci, 'push'), codeql: select(trusted.codeql, 'dynamic') };
}
function statusOf(result, label) {
if (!result || result.status !== 'completed') return { state: 'pending' };
function statusOf(result, label, pendingLabel = label) {
if (!result) return { state: 'pending', reason: `${pendingLabel} run not found for release SHA` };
if (result.status !== 'completed') return { state: 'pending', reason: `${pendingLabel} is ${result.status}` };
return result.conclusion === 'success' ? { state: 'passed' }
: { state: 'failed', reason: `${label} concluded ${result.conclusion}` };
}
@@ -272,7 +275,7 @@ function assessExactShaGates(selected, checks, jobs, inputs) {
const matches = jobs.filter(job => job.name === name);
if (matches.length > 1) throw new Error('Ambiguous required CodeQL job');
const job = matches[0];
if (!job) return { state: 'pending' };
if (!job) return { state: 'pending', reason: `CodeQL job "${name}" missing from selected attempt` };
if (job.run_id !== run.id || job.run_attempt !== run.run_attempt
|| job.head_sha !== inputs.releaseSha || job.head_branch !== 'main') {
throw new Error('CodeQL job does not belong to the selected run attempt');
@@ -281,9 +284,11 @@ function assessExactShaGates(selected, checks, jobs, inputs) {
=== `https://api.github.com/repos/${inputs.repository}/check-runs/${candidate.id}`);
if (!check || check.name !== name || check.head_sha !== inputs.releaseSha
|| check.check_suite.id !== run.check_suite_id || check.app.id !== ACTIONS_APP.id
|| check.app.slug !== ACTIONS_APP.slug) return { state: 'pending' };
for (const result of [job, check]) {
const assessment = statusOf(result, name);
|| check.app.slug !== ACTIONS_APP.slug) {
return { state: 'pending', reason: `CodeQL check "${name}" missing or not bound to trusted job` };
}
for (const [kind, result] of [['job', job], ['check', check]]) {
const assessment = statusOf(result, name, `CodeQL ${kind} "${name}"`);
if (assessment.state !== 'passed') return assessment;
}
}
@@ -301,30 +306,39 @@ async function waitForExactShaGates(inputs, fetchImpl = fetch, sleep = defaultSl
const client = options.client || createGithubClient(inputs, fetchImpl, options);
const attempts = setting(options.attempts ?? process.env.RELEASE_GATE_ATTEMPTS, DEFAULT_ATTEMPTS, DEFAULT_ATTEMPTS);
const delay = setting(options.delayMs ?? process.env.RELEASE_GATE_DELAY_MS, DEFAULT_DELAY_MS, DEFAULT_DELAY_MS);
const trusted = await trustedProducers(client, inputs);
const readRuns = async () => selectRuns(await client.pages(
`/actions/runs?head_sha=${inputs.releaseSha}&branch=main&per_page=100`, 'workflow_runs', runShape
), inputs, trusted);
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const selected = await readRuns();
let assessment = statusOf(selected.ci, 'CI');
if (assessment.state === 'passed') assessment = statusOf(selected.codeql, 'CodeQL');
if (assessment.state === 'passed') {
const run = selected.codeql;
const jobs = await client.pages(`/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100`, 'jobs', jobShape);
const checks = await client.pages(`/check-suites/${run.check_suite_id}/check-runs?filter=all&per_page=100`, 'check_runs', checkShape);
assessment = assessExactShaGates(selected, checks, jobs, inputs);
let lastReason = 'no gate assessment completed';
try {
const trusted = await trustedProducers(client, inputs);
const readRuns = async () => selectRuns(await client.pages(
`/actions/runs?head_sha=${inputs.releaseSha}&branch=main&per_page=100`, 'workflow_runs', runShape
), inputs, trusted);
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const selected = await readRuns();
let assessment = statusOf(selected.ci, 'CI');
if (assessment.state === 'passed') assessment = statusOf(selected.codeql, 'CodeQL');
if (assessment.state === 'passed') {
// Do not approve an attempt superseded while its jobs/checks were read.
const finalRuns = await readRuns();
if (JSON.stringify(finalRuns) === JSON.stringify(selected)) return;
assessment = { state: 'pending' };
const run = selected.codeql;
const jobs = await client.pages(`/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100`, 'jobs', jobShape);
const checks = await client.pages(`/check-suites/${run.check_suite_id}/check-runs?filter=all&per_page=100`, 'check_runs', checkShape);
assessment = assessExactShaGates(selected, checks, jobs, inputs);
if (assessment.state === 'passed') {
// Do not approve an attempt superseded while its jobs/checks were read.
const finalRuns = await readRuns();
if (JSON.stringify(finalRuns) === JSON.stringify(selected)) return;
assessment = { state: 'pending', reason: 'Trusted CI or CodeQL run changed during verification' };
}
}
if (assessment.state === 'failed') throw new Error(assessment.reason);
lastReason = assessment.reason;
if (attempt < attempts) await client.pause(signal => sleep(delay, signal));
}
if (assessment.state === 'failed') throw new Error(assessment.reason);
if (attempt < attempts) await client.pause(signal => sleep(delay, signal));
} catch (error) {
if (error instanceof ReleaseGateDeadlineError) {
throw new Error(`${error.message}; last pending gate: ${lastReason}`, { cause: error });
}
throw error;
}
throw new Error('Timed out waiting for successful exact-SHA CI and CodeQL checks');
throw new Error(`Timed out waiting for successful exact-SHA CI and CodeQL checks; last pending gate: ${lastReason}`);
}
async function main() {
+137 -60
View File
@@ -219,13 +219,19 @@ function fixture() {
}));
return { runs, checks, jobs, workflows: structuredClone(workflows), repo: { ...repoIdentity } };
}
function withItem(data, collection, index, changes) {
return {
...data,
[collection]: data[collection].map((item, position) => position === index ? { ...item, ...changes } : item),
};
}
function response(payload, link = null) {
return { ok: true, status: 200, headers: { get: () => link }, json: async () => payload };
}
function fakeApi(data = fixture(), modify = () => null) {
const calls = [];
let calls = [];
const fetchImpl = async (url, options) => {
calls.push(url);
calls = [...calls, url];
const replacement = modify(url, options, calls);
if (replacement) return replacement;
const pathname = new URL(url).pathname.replace(`/repos/${repository}`, '');
@@ -238,7 +244,7 @@ function fakeApi(data = fixture(), modify = () => null) {
if (pathname === `/git/tags/${tagSha}`) return response({ sha: tagSha, tag: 'v1.2.3', object: { type: 'commit', sha: releaseSha }, verification: { verified: true, reason: 'valid' } });
throw new Error(`Unexpected synthetic API path ${pathname}`);
};
return { fetchImpl, calls };
return { fetchImpl, get calls() { return calls; } };
}
const once = { attempts: 1, timeoutMs: 1000, requestTimeoutMs: 100 };
async function gates(data, modify) {
@@ -263,76 +269,79 @@ test('pre-install verifier loads with built-ins only and still rejects malformed
});
test('complete trusted CI and default CodeQL categories pass without display-name trust', async () => {
const data = fixture();
data.runs[0].name = 'Renamed CI';
data.runs[1].name = 'Push on main';
const data = { ...fixture(), runs: fixture().runs.map((run, index) => ({ ...run, name: index ? 'Push on main' : 'Renamed CI' })) };
const calls = await gates(data);
assert.ok(calls.some(url => url.includes('/attempts/1/jobs')));
assert.ok(calls.some(url => url.includes('/check-suites/101/check-runs')));
});
for (const [name, mutate] of [
['impostor CI workflow', d => { d.runs[0].workflow_id = 999; d.runs[0].name = 'CI'; }],
['wrong workflow path', d => { d.runs[0].path = '.github/workflows/spoof.yml'; }],
['wrong CI event', d => { d.runs[0].event = 'pull_request'; }],
['wrong main branch', d => { d.runs[0].head_branch = 'release/x'; }],
['wrong run SHA', d => { d.runs[0].head_sha = 'c'.repeat(40); }],
['foreign run repository', d => { d.runs[0].repository.id = 1; }],
['foreign head repository', d => { d.runs[0].head_repository.full_name = 'impostor/ECC'; }],
['untrusted check app', d => { d.checks[0].app.id = 1; }],
['wrong app slug', d => { d.checks[0].app.slug = 'spoof'; }],
['wrong check suite', d => { d.checks[0].check_suite.id = 999; }],
['wrong check SHA', d => { d.checks[0].head_sha = 'c'.repeat(40); }],
['missing required category', d => { d.jobs.pop(); }],
['missing bound check', d => { d.checks.pop(); }],
['new pending category', d => { d.jobs.push({ ...d.jobs[0], id: 999, name: 'Analyze (ruby)', status: 'queued', conclusion: null }); }],
['ambiguous category jobs', d => { d.jobs.push({ ...d.jobs[0], id: 999 }); }],
['wrong attempt job', d => { d.jobs[0].run_attempt = 2; }],
['wrong run job', d => { d.jobs[0].run_id = 90; }],
['wrong job branch', d => { d.jobs[0].head_branch = 'feature'; }],
['foreign check URL', d => { d.jobs[0].check_run_url = 'https://api.github.com/repos/spoof/ECC/check-runs/200'; }],
['job name does not match bound check', d => { d.checks[0].name = 'CodeQL'; }],
['ambiguous workflow metadata', d => { d.workflows.push({ ...d.workflows[0], id: 999 }); }],
['inactive trusted workflow', d => { d.workflows[0].state = 'disabled_manually'; }],
]) {
const rejectedFixtures = [
['impostor CI workflow', d => withItem(d, 'runs', 0, { workflow_id: 999, name: 'CI' })],
['wrong workflow path', d => withItem(d, 'runs', 0, { path: '.github/workflows/spoof.yml' })],
['wrong CI event', d => withItem(d, 'runs', 0, { event: 'pull_request' })],
['wrong main branch', d => withItem(d, 'runs', 0, { head_branch: 'release/x' })],
['wrong run SHA', d => withItem(d, 'runs', 0, { head_sha: 'c'.repeat(40) })],
['foreign run repository', d => withItem(d, 'runs', 0, { repository: { ...d.runs[0].repository, id: 1 } })],
['foreign head repository', d => withItem(d, 'runs', 0, { head_repository: { ...d.runs[0].head_repository, full_name: 'impostor/ECC' } })],
['untrusted check app', d => withItem(d, 'checks', 0, { app: { ...d.checks[0].app, id: 1 } })],
['wrong app slug', d => withItem(d, 'checks', 0, { app: { ...d.checks[0].app, slug: 'spoof' } })],
['wrong check suite', d => withItem(d, 'checks', 0, { check_suite: { id: 999 } })],
['wrong check SHA', d => withItem(d, 'checks', 0, { head_sha: 'c'.repeat(40) })],
['missing required category', d => ({ ...d, jobs: d.jobs.slice(0, -1) })],
['missing bound check', d => ({ ...d, checks: d.checks.slice(0, -1) })],
['new pending category', d => ({ ...d, jobs: [...d.jobs, { ...d.jobs[0], id: 999, name: 'Analyze (ruby)', status: 'queued', conclusion: null }] })],
['ambiguous category jobs', d => ({ ...d, jobs: [...d.jobs, { ...d.jobs[0], id: 999 }] })],
['wrong attempt job', d => withItem(d, 'jobs', 0, { run_attempt: 2 })],
['wrong run job', d => withItem(d, 'jobs', 0, { run_id: 90 })],
['wrong job branch', d => withItem(d, 'jobs', 0, { head_branch: 'feature' })],
['foreign check URL', d => withItem(d, 'jobs', 0, { check_run_url: 'https://api.github.com/repos/spoof/ECC/check-runs/200' })],
['job name does not match bound check', d => withItem(d, 'checks', 0, { name: 'CodeQL' })],
['ambiguous workflow metadata', d => ({ ...d, workflows: [...d.workflows, { ...d.workflows[0], id: 999 }] })],
['inactive trusted workflow', d => withItem(d, 'workflows', 0, { state: 'disabled_manually' })],
];
for (const [name, change] of rejectedFixtures) {
test(`release gate rejects ${name}`, async () => {
const data = fixture(); mutate(data);
const original = fixture();
const snapshot = structuredClone(original);
const data = change(original);
assert.deepStrictEqual(original, snapshot, 'fixture variants must leave their input unchanged');
await assert.rejects(gates(data));
});
}
for (const conclusion of ['failure', 'cancelled', 'skipped', 'neutral', 'timed_out', 'action_required']) {
test(`required trusted check ${conclusion} fails despite newer spoof success`, async () => {
const data = fixture();
data.checks[0].conclusion = conclusion;
data.checks.push({ ...data.checks[0], id: 999, conclusion: 'success', app: { id: 1, slug: 'spoof' } });
const base = withItem(fixture(), 'checks', 0, { conclusion });
const data = { ...base, checks: [...base.checks, { ...base.checks[0], id: 999, conclusion: 'success', app: { id: 1, slug: 'spoof' } }] };
await assert.rejects(gates(data), /concluded/);
});
}
test('newer display-name impostor cannot replace a failed trusted CI run', async () => {
const data = fixture(); data.runs[0].conclusion = 'failure';
data.runs.push({ ...data.runs[0], id: 999, workflow_id: 999, name: 'CI', conclusion: 'success' });
const base = withItem(fixture(), 'runs', 0, { conclusion: 'failure' });
const data = { ...base, runs: [...base.runs, { ...base.runs[0], id: 999, workflow_id: 999, name: 'CI', conclusion: 'success' }] };
await assert.rejects(gates(data), /CI concluded failure/);
});
test('workflow IDs come from exact-path metadata and unrelated spoof results do not gate', async () => {
const data = fixture();
data.workflows.forEach((workflow, index) => { workflow.id = 900 + index; data.runs[index].workflow_id = workflow.id; });
data.runs.push({ ...data.runs[0], id: 999, workflow_id: 999, name: 'CI', conclusion: 'failure' });
data.checks.push({ ...data.checks[0], id: 999, conclusion: 'failure', app: { id: 1, slug: 'spoof' } });
const base = fixture();
const data = {
...base,
workflows: base.workflows.map((workflow, index) => ({ ...workflow, id: 900 + index })),
runs: [...base.runs.map((run, index) => ({ ...run, workflow_id: 900 + index })), { ...base.runs[0], id: 999, workflow_id: 999, name: 'CI', conclusion: 'failure' }],
checks: [...base.checks, { ...base.checks[0], id: 999, conclusion: 'failure', app: { id: 1, slug: 'spoof' } }],
};
await gates(data);
});
test('newer trusted pending run does not reuse older success', async () => {
const data = fixture();
data.runs.push({ ...data.runs[1], id: 12, status: 'queued', conclusion: null });
const base = fixture();
const data = { ...base, runs: [...base.runs, { ...base.runs[1], id: 12, status: 'queued', conclusion: null }] };
await assert.rejects(gates(data), /Timed out|deadline/);
});
test('newer trusted attempt cannot reuse previous attempt jobs', async () => {
const data = fixture();
data.runs[1].run_attempt = 2;
const data = withItem(fixture(), 'runs', 1, { run_attempt: 2 });
await assert.rejects(gates(data, url => url.includes('/attempts/2/jobs') ? response({ total_count: 2, jobs: data.jobs.slice(0, 2).map(job => ({ ...job, run_attempt: 2 })) }) : null));
});
@@ -347,7 +356,7 @@ test('a new trusted run appearing during collection fails readiness', async () =
});
test('failure on a later check page cannot be hidden', async () => {
const data = fixture(); data.checks[2].conclusion = 'failure';
const data = withItem(fixture(), 'checks', 2, { conclusion: 'failure' });
await assert.rejects(gates(data, url => {
if (!url.includes('/check-runs?')) return null;
return url.includes('page=2')
@@ -356,6 +365,68 @@ test('failure on a later check page cannot be hidden', async () => {
}), /concluded failure/);
});
// Pending diagnostics must identify the blocked gate without changing its decision.
for (const [name, change, reason] of [
['missing CI', d => ({ ...d, runs: d.runs.slice(1) }), 'CI run not found for release SHA'],
['missing CodeQL', d => ({ ...d, runs: d.runs.slice(0, 1) }), 'CodeQL run not found for release SHA'],
['running CI', d => withItem(d, 'runs', 0, { status: 'in_progress', conclusion: null }), 'CI is in_progress'],
['queued CodeQL', d => withItem(d, 'runs', 1, { status: 'queued', conclusion: null }), 'CodeQL is queued'],
['missing job', d => ({ ...d, jobs: d.jobs.slice(0, 2) }), 'CodeQL job "Analyze (python)" missing from selected attempt'],
['missing check', d => ({ ...d, checks: d.checks.slice(0, 2) }), 'CodeQL check "Analyze (python)" missing or not bound to trusted job'],
['untrusted check', d => withItem(d, 'checks', 0, { app: { id: 1, slug: 'spoof' } }), 'CodeQL check "Analyze (actions)" missing or not bound to trusted job'],
['running job', d => withItem(d, 'jobs', 0, { status: 'in_progress', conclusion: null }), 'CodeQL job "Analyze (actions)" is in_progress'],
['queued check', d => withItem(d, 'checks', 0, { status: 'queued', conclusion: null }), 'CodeQL check "Analyze (actions)" is queued'],
]) {
test(`attempt exhaustion diagnoses ${name}`, async () => {
const original = fixture();
const snapshot = structuredClone(original);
await assert.rejects(gates(change(original)), error => {
assert.match(error.message, /Timed out/);
assert.ok(error.message.endsWith(`last pending gate: ${reason}`), error.message);
assert.ok(!error.message.includes(inputs.token));
return true;
});
assert.deepStrictEqual(original, snapshot);
});
}
test('attempt exhaustion reports a superseded trusted run', async () => {
const data = fixture(); let reads = 0;
await assert.rejects(gates(data, url => url.includes('/actions/runs?') && ++reads === 2
? response({ total_count: 3, workflow_runs: [...data.runs, { ...data.runs[1], id: 12, status: 'queued', conclusion: null }] })
: null), /last pending gate: Trusted CI or CodeQL run changed during verification$/);
});
test('global deadline reports the latest pending gate and preserves its cause', async () => {
const data = withItem(fixture(), 'runs', 0, { status: 'queued', conclusion: null });
let reads = 0; let clock = 0;
const api = fakeApi(data, url => url.includes('/actions/runs?') && ++reads > 1
? response({ total_count: 1, workflow_runs: [{ ...data.runs[0], status: 'completed', conclusion: 'success' }] })
: null);
await assert.rejects(waitForExactShaGates(inputs, api.fetchImpl, async delay => { clock += delay; }, {
attempts: 3, delayMs: 10, timeoutMs: 15, requestTimeoutMs: 10, now: () => clock,
}), error => {
assert.match(error.message, /deadline exceeded; last pending gate: CodeQL run not found for release SHA$/);
assert.match(error.cause.message, /deadline exceeded$/);
return true;
});
assert.strictEqual(reads, 2);
});
test('request deadline before assessment reports that no gate was assessed', async () => {
let signal;
await assert.rejects(waitForExactShaGates(inputs, async (_url, options) => {
signal = options.signal;
return { ...response(null), json: () => new Promise(() => {}) };
}, async () => {}, { ...once, requestTimeoutMs: 5 }), /deadline exceeded; last pending gate: no gate assessment completed$/);
assert.strictEqual(signal.aborted, true);
});
test('non-deadline errors retain identity even when their message mentions deadline', async () => {
const failure = new Error('synthetic deadline text is not a timeout classification');
await assert.rejects(gates(fixture(), () => { throw failure; }), error => error === failure);
});
test('API requests reject redirects and carry abort signals without dependency loading', async () => {
const api = fakeApi(fixture(), (_url, options) => {
assert.strictEqual(options.redirect, 'error');
@@ -378,38 +449,44 @@ test('release input and retry bounds reject unsafe or unbounded values', () => {
});
test('an aborted global deadline covers a stalled retry sleep', async () => {
const data = fixture(); data.runs[0].status = 'queued'; data.runs[0].conclusion = null;
const data = withItem(fixture(), 'runs', 0, { status: 'queued', conclusion: null });
let signal;
await assert.rejects(waitForExactShaGates(inputs, fakeApi(data).fetchImpl, (_delay, provided) => {
signal = provided;
assert.ok(signal instanceof AbortSignal, 'abort signal required');
return new Promise(() => {});
}, { attempts: 2, timeoutMs: 20, requestTimeoutMs: 10 }), /deadline/);
}, { attempts: 2, timeoutMs: 20, requestTimeoutMs: 10 }), /deadline exceeded; last pending gate: CI is queued$/);
assert.strictEqual(signal.aborted, true);
});
test('signed annotated tag binds full ref, object SHA, name and direct commit', async () => {
assert.strictEqual(await verifySignedAnnotatedTag(inputs, fakeApi().fetchImpl), tagSha);
});
for (const [name, pathPart, mutate] of [
['different ref', '/git/ref/', p => { p.ref = 'refs/tags/v0.0.0'; }],
['lightweight tag', '/git/ref/', p => { p.object.type = 'commit'; }],
['malformed object SHA', '/git/ref/', p => { p.object.sha = 'bad'; }],
['wrong signed name', '/git/tags/', p => { p.tag = 'v0.0.0'; }],
['wrong returned object SHA', '/git/tags/', p => { p.sha = 'c'.repeat(40); }],
['unverified signature', '/git/tags/', p => { p.verification.verified = false; }],
['invalid verification reason', '/git/tags/', p => { p.verification.reason = 'unsigned'; }],
['nested tag', '/git/tags/', p => { p.object.type = 'tag'; }],
['wrong target commit', '/git/tags/', p => { p.object.sha = 'c'.repeat(40); }],
for (const [name, pathPart, change] of [
['different ref', '/git/ref/', p => ({ ...p, ref: 'refs/tags/v0.0.0' })],
['lightweight tag', '/git/ref/', p => ({ ...p, object: { ...p.object, type: 'commit' } })],
['malformed object SHA', '/git/ref/', p => ({ ...p, object: { ...p.object, sha: 'bad' } })],
['wrong signed name', '/git/tags/', p => ({ ...p, tag: 'v0.0.0' })],
['wrong returned object SHA', '/git/tags/', p => ({ ...p, sha: 'c'.repeat(40) })],
['unverified signature', '/git/tags/', p => ({ ...p, verification: { ...p.verification, verified: false } })],
['invalid verification reason', '/git/tags/', p => ({ ...p, verification: { ...p.verification, reason: 'unsigned' } })],
['nested tag', '/git/tags/', p => ({ ...p, object: { ...p.object, type: 'tag' } })],
['wrong target commit', '/git/tags/', p => ({ ...p, object: { ...p.object, sha: 'c'.repeat(40) } })],
]) {
test(`signed tag rejects ${name}`, async () => {
const base = fakeApi();
let observed = [];
await assert.rejects(verifySignedAnnotatedTag(inputs, async (url, options) => {
const result = await base.fetchImpl(url, options);
const payload = await result.json();
if (url.includes(pathPart)) mutate(payload);
return response(payload);
const snapshot = structuredClone(payload);
const changed = url.includes(pathPart) ? change(payload) : payload;
observed = [...observed, { payload, snapshot }];
return response(changed);
}));
for (const { payload, snapshot } of observed) {
assert.deepStrictEqual(payload, snapshot, 'tag variants must leave their input unchanged');
}
});
}
@@ -488,7 +565,7 @@ test('stalled headers and response bodies are aborted by the request deadline',
});
test('global deadline includes retries and prevents further requests', async () => {
const data = fixture(); data.runs[0].status = 'queued'; data.runs[0].conclusion = null;
const data = withItem(fixture(), 'runs', 0, { status: 'queued', conclusion: null });
let clock = 0; let sleeps = 0;
await assert.rejects(waitForExactShaGates(inputs, fakeApi(data).fetchImpl, async delay => { clock += delay; sleeps += 1; }, {
attempts: 5, delayMs: 10, timeoutMs: 15, requestTimeoutMs: 10, now: () => clock,