Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/scripts/work-unblock/label-unblocked.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ const STAGE_STEP = "Stage the activity from the base";
// request-lane-token's notice when kill-switch-ok ends a kill-switch refusal
// green with no token.
const STOPPED = /^\d{4}-\d\d-\d\dT[\d:.]+Z ##\[notice\]lane-token-stopped: the broker refused the token because a lanes kill switch is on(?:,|$)/;
// The runner's markers around request-lane-token's one composite step, its
// `Request the lane token` with id `request`; a STOPPED line counts only
// between the job's first start marker and its end marker.
const MINT_START = /^\d{4}-\d\d-\d\dT[\d:.]+Z ##\[start-action display=Request the lane token;id=([\w-]+\.request)\]$/;
const mintEnd = (id) => new RegExp(`^\\d{4}-\\d\\d-\\d\\dT[\\d:.]+Z ##\\[end-action id=${id.replace(".", "\\.")};`);
const BREAKER = "kill-switch";
// The refused jobs' logs on 2026-10-24 were 45 KB (broker-check) and 94 KB
// (apply-issue-patch); a longer one is left unread.
Expand Down Expand Up @@ -1130,10 +1135,11 @@ async function didNoWork(jobsOf, refusalOf, runs) {

// The reason the broker refused `job`, from its log: the failed step's last
// line before the runner's first `Process completed`, in `mode` (check or
// token), or BREAKER for a `stop` mode log with a STOPPED notice line. Null
// when there is no such line or the log cannot be read within LOG_CAP_BYTES,
// or no log reader was given; the log is data, and no other line of it is
// read.
// token), or BREAKER for a `stop` mode log with a STOPPED line inside the
// mint step's MINT_START section. Null when there is no such line or the log
// cannot be read within LOG_CAP_BYTES, or no log reader was given; the log is
// data: check and token modes read only that one line, and a STOPPED line
// outside the section never counts.
function refusalReader(jobLog, repo) {
return async (job, mode) => {
if (!jobLog || !RUN_ID.test(String(job?.id))) {
Expand All @@ -1148,7 +1154,13 @@ function refusalReader(jobLog, repo) {
}
const lines = String(log ?? "").split("\n");
if (mode === "stop") {
return lines.some((line) => STOPPED.test(line)) ? BREAKER : null;
const start = lines.findIndex((line) => MINT_START.test(line));
if (start < 0) {
return null;
}
const end = mintEnd(MINT_START.exec(lines[start])[1]);
const stop = lines.findIndex((line, at) => at > start && end.test(line));
return stop > 0 && lines.slice(start, stop).some((line) => STOPPED.test(line)) ? BREAKER : null;
}
const end = lines.findIndex((line) => STEP_FAILED.test(line));
return end > 0 ? (DENIAL[mode].exec(lines[end - 1])?.[1] ?? null) : null;
Expand Down
29 changes: 27 additions & 2 deletions .github/scripts/work-unblock/label-unblocked.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,10 @@ test("breaker trips at the mint never count toward the failed-mint bound", async
// With request-lane-token's kill-switch-ok, the same refusal ends the mint
// step green with no token and a `lane-token-stopped` notice: every step that
// keys on the token, `Stage the activity from the base` first, skips, and the
// run ends green. Only that shape in the job listing earns a log read.
// run ends green. Only that shape in the job listing earns a log read. The
// log's shape is job 120970852314's: the composite's one step sits between a
// start-action and an end-action marker, and the step's own output follows
// its script group.
const stoppedJob = (id, name) => ({
id,
name,
Expand All @@ -2352,8 +2355,13 @@ const stoppedJob = (id, name) => ({
});
const stoppedLog = ({ stamp = "2026-10-30T01:57:51.9466327Z ", line = "lane-token-stopped: the broker refused the token because a lanes kill switch is on, a designed stop; this run takes no token and skips the rest" } = {}) =>
[
"2026-10-30T01:57:50.6158116Z ##[group]Run request-lane-token",
"2026-10-30T01:57:50.6157454Z ##[group]Run ./.base/.github/actions/request-lane-token",
"2026-10-30T01:57:50.6159328Z ##[endgroup]",
"2026-10-30T01:57:50.6165385Z ##[start-action display=Request the lane token;id=lane-token.request]",
"2026-10-30T01:57:50.6182178Z ##[group]Run set -euo pipefail",
"2026-10-30T01:57:50.6340248Z ##[endgroup]",
`${stamp}##[notice]${line}`,
"2026-10-30T01:57:51.9483644Z ##[end-action id=lane-token.request;outcome=success;conclusion=success;duration_ms=1331]",
"2026-10-30T01:57:52.0000000Z ##[group]Run jq -n \\",
"",
].join("\n");
Expand Down Expand Up @@ -2425,6 +2433,23 @@ test("a stopped mint whose log does not carry the kill-switch notice is a failed
}
});

test("a kill-switch notice outside the mint step's section is a failed mint, never a breaker trip", async () => {
const [head, tail] = stoppedLog().split(/^.*##\[notice\].*\n/m);
const notice = stoppedLog().split("\n").find((line) => line.includes("##[notice]"));
const start = head.split("\n").find((line) => line.includes("##[start-action"));
const end = tail.split("\n").find((line) => line.includes("##[end-action"));
for (const [name, log] of [
["in a later step", `${head}${tail}${notice}\n`],
["before the mint step", `${notice}\n${head}${tail}`],
["in a second mint block after the first", `${head}${tail}${start}\n${notice}\n${end}\n`],
["with no end-action marker", `${head}${notice}\n`],
]) {
const { github, actions, calls } = stalledRun({ jobs: MINT_STOPPED, logs: { [PATCH_MINT_ID]: log }, conclusion: "success" });
assert.equal(await sweep({ github, actions }), 0, name);
assert.deepEqual(writes(calls), refired(30), name);
}
});

test("breaker trips past limit-stops-per-issue since a person last added agent-ready wait on the stranded backoff", async () => {
const under = stalledRun({ jobs: BROKER_REFUSED, logs: CHECK_KILLED, ...(await afterSweeps(2, BROKER_REFUSED, CHECK_KILLED)) });
assert.equal(await sweep({ github: under.github, actions: under.actions }), 0);
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/pr-run-activity-read.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2097,8 +2097,10 @@ jobs:
if ((RESUME_BRANCH === '') !== (RESUME_TIP === '') || (RESUME_TIP !== '' && STARTED !== 'true')) return bad('resume-tip');
if (STARTED === 'true' && BASE_SHA === '') return bad('base-sha');
// Branches to prune: at most 20 distinct lane branches of the
// issue, none of them the reuse or resume branch. Not re-derived:
// the prune job re-checks each against GitHub before deleting.
// issue, none of them the reuse or resume branch. The checked step
// publishes only the names the re-derived earlier-work check also
// names, and the prune job re-checks each against GitHub before
// deleting.
const prune = PRUNE_BRANCHES === '' ? [] : PRUNE_BRANCHES.split(' ');
if (prune.length > 20 || new Set(prune).size !== prune.length
|| prune.some((name) => !BRANCH.test(name) || name === REUSE_BRANCH || name === RESUME_BRANCH)) return bad('prune-branches');
Expand Down
Loading