fix(ci): file main CI failures whose successor has not settled (#15471)

* fix(ci): file main CI failures whose successor has not settled

The guard added in #15272 treats a queued or in-progress successor at
main's tip as owning the result, so the report is skipped. A successor
that later ends cancelled, timed_out or skipped never re-enters the
workflow — its gate admits only failures — so the failure is never
reported at all.

That is how 10 main-push failures between 14:00Z and 17:30Z on
2026-10-24 produced no issue (#15470), with 65 of the last 100 Qwen
Code CI push runs on main ending cancelled.

The reporter cannot wait for the successor either: it runs on
ubuntu-latest, and that pool queued it for 20-100 minutes in the same
window. Report instead, marked provisional, since the per-signature
route hands new issues to a human rather than to autofix: a stale
provisional report costs a close, a dropped one costs the regression.

* fix(ci): keep a provisional head off the infra rerun path

Four review findings on d98bc2d1ca:

- rerun_infra holds actions: write but was gated only on infra_only and
  file_issue. An unsettled successor now yields provisional=true instead
  of superseded=true, so destination runs and file_issue becomes true: an
  infra-only failure at a head main already replaced would dispatch
  rerun-failed-jobs onto the contended pool this PR cites (#15470), and a
  successful dispatch would then satisfy file_issue's
  `rerun_infra.result != 'success'` operand, so nothing was filed and no
  step summary was written either. Add the provisional operand; skipping
  the rerun is what makes file_issue record it instead.
- Only a settled successor can own the result, so invert the case arm:
  anything but `completed` (and `none`) is provisional. The enumeration
  covered queued and in_progress only, leaving waiting, requested and
  pending to file without the caveat and to warn falsely that the
  successor "does not report".
- The caveat heads the comment routes as well as the create routes, so
  "this issue can be closed" was advice to close a tracker belonging to a
  different occurrence. Reword to "this report can be dismissed".
- Pin both YAML hops of the provisional wire; retyping either left
  PROVISIONAL empty and the suite green.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvmwqg38tp

* test(ci): cover the provisional caveat on its two untested write sites

The caveat is prepended once ahead of the route split, so it reaches three
write sites: the per-signature certified-miss create, the per-signature
recurrence comment, and the aggregate route. Only the first was exercised
with provisional=true, so narrowing the prepend to a single branch would
drop the caveat from the other two unnoticed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvn35xgdtw

---------

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
易良andQwen-Coder authored and GitHub committed 2026-10-25 12:51:30 +08:00
1 parent 1a98854b76
commit cf3ba47dfd
5 files changed
+205 -56

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@
16647 live-host-release.yml
5950 live-host.yml
14543 managed-agent-gate.yml
53149 main-ci-failure-issue.yml
55007 main-ci-failure-issue.yml
1343 mobile-shell.yml
2277 pnpm-store.yml
2489 pnpm-worktree-smoke.yml
+46 -11
View File
@@ -70,6 +70,9 @@ jobs:
# record to the workflow's aggregate issue downstream.
signature_title: '${{ steps.summary.outputs.signature_title }}'
test_markers: '${{ steps.summary.outputs.test_markers }}'
# The newer run at main's tip had not settled when this report was
# prepared. The issue is still filed, with a note saying so.
provisional: '${{ steps.live.outputs.provisional }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
@@ -96,6 +99,7 @@ jobs:
run: |-
set -euo pipefail
superseded=false
provisional=false
if live_attempt="$(
gh api "repos/${REPO}/actions/runs/${WORKFLOW_RUN_ID}" \
--jq '.run_attempt'
@@ -114,15 +118,26 @@ jobs:
# repair issue for a head main has already replaced, which is how
# #15259 was filed after #15128 fixed its host-lock timeout.
#
# The successor must also be able to report the result. The job gate
# above admits only `failure` conclusions, so a run that ends
# Only a SETTLED successor can own the result. The job gate above
# admits only `failure` conclusions, so a successor that ends
# cancelled, timed_out, skipped or startup_failure never re-enters
# this workflow; crediting its mere existence would drop the only
# report of a real regression. A failed successor reports itself, and
# a queued or in-progress one is a genuine deferral. A path-filtered
# push arm (SDK Python) or a `[skip ci]` commit leaves no run at the
# new tip at all, and that also reports here. Read failures keep the
# old fail-open reporting.
# report of a real regression. A completed success means the tree is
# green at the tip, and a completed failure reports itself.
#
# An unsettled successor proves nothing yet, and waiting for it is
# not possible here: this job runs on a pool that queued it
# for 20-100 minutes (#15470), so the tip's run is usually still
# young when the report is prepared, and the successor's own re-entry
# only covers the failure conclusion. Reporting is therefore the
# lesser mistake, marked provisional: the per-signature route files
# those for a human to triage and never routes them to autofix
# (#15405), so a stale one costs a close, while a dropped one costs
# the regression.
#
# A path-filtered push arm (SDK Python) or a `[skip ci]` commit
# leaves no run at the new tip at all, and that also reports here.
# Read failures keep the old fail-open reporting.
#
# Every attempt is compared, not just the first: #15259 itself was
# filed by such an attempt-2 recovery run. The successor check is
@@ -139,20 +154,26 @@ jobs:
)"; then
read -r successor_status successor_conclusion <<< "${successor}"
successor_owns=false
# Complement, not an enumeration: only a settled successor
# owns the result, and not-yet-final statuses grow.
case "${successor_status}" in
queued | in_progress)
successor_owns=true
;;
completed)
case "${successor_conclusion}" in
success | failure) successor_owns=true ;;
esac
;;
none)
;;
*)
provisional=true
;;
esac
if [[ "${successor_owns}" == 'true' ]]; then
superseded=true
echo "::warning::Run ${WORKFLOW_RUN_ID} tested ${HEAD_SHA}, but main is now ${tip}; the newer push run owns its result."
echo "Superseded main-CI failure: run ${WORKFLOW_RUN_ID} tested ${HEAD_SHA}, but main is now ${tip}; the newer push run owns the result." >> "${GITHUB_STEP_SUMMARY}"
elif [[ "${provisional}" == 'true' ]]; then
echo "::warning::Run ${WORKFLOW_RUN_ID} tested ${HEAD_SHA} and main is now ${tip}; its newer push run is ${successor_status}, so this report is provisional."
elif [[ "${successor_status}" == 'none' ]]; then
echo "::warning::Run ${WORKFLOW_RUN_ID} tested ${HEAD_SHA} and main is now ${tip}, but workflow ${WORKFLOW_ID} has no newer push run there; reporting this run."
else
@@ -167,6 +188,7 @@ jobs:
fi
fi
echo "superseded=${superseded}" >> "${GITHUB_OUTPUT}"
echo "provisional=${provisional}" >> "${GITHUB_OUTPUT}"
- name: 'Select the failure destination'
id: 'destination'
@@ -401,10 +423,12 @@ jobs:
# only for destinations that file (main runs and merged PRs). An open PR's
# infra death is reported on the PR without dispatching a rerun, and the
# classifier never grants an untrusted run actions: write.
# Not for a provisional report: that head is already replaced, and its rerun
# could not re-enter here — skipping lets file_issue record it instead.
rerun_infra:
name: 'Rerun infrastructure-only failures'
needs: 'analyze'
if: "${{ needs.analyze.outputs.infra_only == 'true' && needs.analyze.outputs.file_issue == 'true' }}"
if: "${{ needs.analyze.outputs.infra_only == 'true' && needs.analyze.outputs.file_issue == 'true' && needs.analyze.outputs.provisional != 'true' }}"
runs-on: 'ubuntu-latest'
timeout-minutes: 5
permissions:
@@ -457,9 +481,20 @@ jobs:
RUN_ATTEMPT: '${{ github.event.workflow_run.run_attempt }}'
SIGNATURE_TITLE: '${{ needs.analyze.outputs.signature_title }}'
TEST_MARKERS: '${{ needs.analyze.outputs.test_markers }}'
PROVISIONAL: '${{ needs.analyze.outputs.provisional }}'
RUN_AT: '${{ github.event.workflow_run.updated_at }}'
run: |-
set -euo pipefail
if [[ "${PROVISIONAL}" == 'true' ]]; then
# Heads every route's body — both create paths and both
# comment-on-existing paths — so it must not assume this issue is
# the tracker for this occurrence.
ISSUE_BODY="$(
printf '%s\n\n%s' \
'> **Provisional.** Main advanced past this head while the report was being prepared, and the newer push run at the tip had not finished. If that run passes, this failure is already covered and this report can be dismissed.' \
"${ISSUE_BODY}"
)"
fi
title="Main CI failed: ${WORKFLOW_NAME}"
marker="<!-- main-ci-failure-aggregate:${WORKFLOW_ID} -->"
# Embedded in every per-signature filing (and accreted onto every
+13 -6
View File
@@ -184,12 +184,19 @@ which has had no `pull_request` trigger since #14205.
A post-merge push failure whose head is no longer main's tip is recorded only
on the analyze run's step summary, not on the workflow issue, when a newer push
run of the same workflow exists at the new tip and can report the result
itself. Queued and in-progress successors count as a deferral, as do
successors that completed `success` or `failure` — a failed successor's own
completion event re-enters this workflow. A successor that ended `cancelled`,
`timed_out`, `skipped`, or `startup_failure` is never analyzed, so the original
failure is reported instead.
run of the same workflow exists at the new tip and has already settled: a
`success` there means the tree is green, and a `failure` reports itself through
its own completion event. A successor that ended `cancelled`, `timed_out`,
`skipped`, or `startup_failure` is never analyzed, so the original failure is
reported instead.
A successor that has not completed settles nothing, and this job cannot wait
for it — the pool the reporter runs on queued it for 20-100
minutes, so the tip's run is usually still young when the report is prepared.
The failure is reported anyway, marked provisional at the top of the body. That
is affordable because the per-signature route hands new issues to a human
rather than to autofix: a stale provisional report costs a close, while a
dropped one costs the regression (#15470).
## Cost
@@ -152,11 +152,16 @@ workflow 共用的汇总 issue 上追加评论,找到匹配的已关闭 issue
起已没有 `pull_request` 触发。
当合入后 push 失败的 head 已不再是 main 的 tip 时,如果新 tip 上存在同一 workflow
的更新 push 运行、且它能自行上报结果,这次失败只记录在 analyze 运行的步骤摘要里,
不会记录到 workflow issue 上。仍在排队或运行中的后继算作延迟上报;以 `success` 或
`failure` 结束的后继同样算数——失败后继自己的完成事件会重新进入本 workflow。以
`cancelled`、`timed_out`、`skipped` 或 `startup_failure` 结束的后继永远不会被分析,
因此此时仍然上报原始失败。
的更新 push 运行、且它**已经定论**,这次失败只记录在 analyze 运行的步骤摘要里,不会
记录到 workflow issue 上:`success` 表示该处代码是绿的,`failure` 会通过自己的完成
事件上报。以 `cancelled`、`timed_out`、`skipped` 或 `startup_failure` 结束的后继永远
不会被分析,因此此时仍然上报原始失败。
尚未 `completed` 的后继什么都证明不了,而本 job 也等不起它——报告所在
的 runner 池把它排了 20-100 分钟,准备好报告时 tip 上的运行通常刚开始不久。此时仍然
上报,并在正文顶部标注为临时性(provisional)。之所以负担得起,是因为按签名路由的新
issue 交给人工分诊而不是 autofix:一条过期的临时报告只需关掉,丢掉一条则丢掉整个回归
(#15470)。
## 成本
@@ -397,7 +397,7 @@ fi
},
{ mainTip: 'new-head' },
);
expect(resolved.output).toBe('superseded=true\n');
expect(resolved.output).toBe('superseded=true\nprovisional=false\n');
expect(resolved.stdout).toContain('::warning::');
expect(resolved.stdout).not.toContain('main is now');
expect(resolved.calls).not.toContain('commits/main');
@@ -428,22 +428,22 @@ fi
);
});
it('reports the attempt that is still the latest', () => {
expect(detect('1').output).toBe('superseded=false\n');
expect(detect('1').output).toBe('superseded=false\nprovisional=false\n');
expect(detect('2', { RUN_ATTEMPT: '2' }).output).toBe(
'superseded=false\n',
'superseded=false\nprovisional=false\n',
);
// Stale-attempt supersession is independent of the tip comparison:
// a bare event never reaches the push arm, and the nightly schedule
// arm is exempt from it. Folding the attempt check inside that arm
// would file an issue for an already-green rerun (#14768's shape).
expect(detect('2').output).toBe('superseded=true\n');
expect(detect('2').output).toBe('superseded=true\nprovisional=false\n');
expect(
detect('2', {
EVENT_NAME: 'schedule',
HEAD_BRANCH: 'main',
HEAD_SHA: 'old-head',
}).output,
).toBe('superseded=true\n');
).toBe('superseded=true\nprovisional=false\n');
});
it('skips a push run when main has advanced past its head', () => {
// Run 39777472391's push run tested 06e59666, main moved on to
@@ -460,7 +460,7 @@ fi
},
{ mainTip: '3d65e23cf62e00da033dccb8f60f9cfdd9c1b76e' },
);
expect(resolved.output).toBe('superseded=true\n');
expect(resolved.output).toBe('superseded=true\nprovisional=false\n');
expect(resolved.stdout).toContain(
'main is now 3d65e23cf62e00da033dccb8f60f9cfdd9c1b76e',
);
@@ -487,7 +487,7 @@ fi
},
{ mainTip: 'new-head', successor: 'none' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain('no newer push run');
expect(resolved.calls).toContain(
'actions/workflows/12345/runs?event=push&branch=main&head_sha=new-head&per_page=1',
@@ -506,25 +506,43 @@ fi
},
{ mainTip: 'new-head', successor: 'completed cancelled' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain('completed cancelled');
expect(resolved.stdout).toContain('does not report');
expect(resolved.summary).toBe('');
});
it('skips a push run while its successor is still queued', () => {
// A queued or in-progress successor is a genuine deferral: its own
// completion event reports the result if it fails.
const resolved = detect(
'1',
{
EVENT_NAME: 'push',
HEAD_BRANCH: 'main',
HEAD_SHA: 'old-head',
},
{ mainTip: 'new-head', successor: 'in_progress' },
);
expect(resolved.output).toBe('superseded=true\n');
expect(resolved.stdout).toContain('the newer push run owns its result');
it('reports a push run whose successor has not settled, flagged provisional', () => {
// A running successor proves nothing yet, and this job cannot wait for
// it: the hosted pool queued the reporter for 20-100 minutes, so the
// tip's run is usually young. The successor's own re-entry covers only
// the failure conclusion, so discarding here loses a real regression
// whenever that run ends cancelled (#15470). The report is filed with
// the caveat instead, and the per-signature route hands it to a human
// rather than autofix, so a stale one costs a close.
// Every not-yet-final status counts, matching the vocabulary
// qwen-autofix.yml uses for the same question.
for (const successor of [
'in_progress',
'queued',
'waiting',
'requested',
'pending',
]) {
const resolved = detect(
'1',
{
EVENT_NAME: 'push',
HEAD_BRANCH: 'main',
HEAD_SHA: 'old-head',
},
{ mainTip: 'new-head', successor },
);
expect(resolved.output).toBe('superseded=false\nprovisional=true\n');
expect(resolved.stdout).toContain(
`${successor}, so this report is provisional`,
);
expect(resolved.summary).toBe('');
}
});
it('fails open when the newer-run lookup cannot be read', () => {
const resolved = detect(
@@ -536,7 +554,7 @@ fi
},
{ mainTip: 'new-head', failCall: 'newer-runs' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain(
'Could not list the newer runs of workflow 12345',
);
@@ -555,7 +573,7 @@ fi
},
{ mainTip: 'new-head', successor: 'completed failure' },
);
expect(resolved.output).toBe('superseded=true\n');
expect(resolved.output).toBe('superseded=true\nprovisional=false\n');
expect(resolved.stdout).toContain('main is now new-head');
expect(resolved.calls).toContain('commits/main');
});
@@ -573,7 +591,7 @@ fi
},
{ mainTip: 'new-head', successor: 'none' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain('no newer push run');
expect(resolved.calls).toContain('commits/main');
});
@@ -587,7 +605,7 @@ fi
},
{ mainTip: 'tested-head' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).not.toContain('::warning::');
expect(resolved.calls).toContain('commits/main');
});
@@ -603,7 +621,7 @@ fi
},
{ mainTip: '' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
});
it('does not compare a scheduled run with the tip of main', () => {
// A schedule runs at its own head and is the only signal macOS and
@@ -618,7 +636,7 @@ fi
},
{ mainTip: 'new-head' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.calls).not.toContain('commits/main');
});
it.each([['pull_request'], ['pull_request_review'], ['workflow_dispatch']])(
@@ -637,7 +655,7 @@ fi
},
{ mainTip: 'new-head' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.calls).not.toContain('commits/main');
},
);
@@ -651,7 +669,7 @@ fi
},
{ mainTip: 'new-head', failCall: 'tip' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain('Could not read the tip of main');
expect(resolved.calls).toContain('commits/main');
});
@@ -667,7 +685,7 @@ fi
},
{ mainTip: 'new-head' },
);
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.calls).not.toContain('commits/main');
});
it('fails open when the live run cannot be read', () => {
@@ -675,7 +693,7 @@ fi
// for a real failure beats silently dropping it.
const gh = `#!/usr/bin/env bash\nprintf '%s' '{"message":"Bad credentials"}'\nexit 1\n`;
const resolved = detect('', {}, { ghScript: gh });
expect(resolved.output).toBe('superseded=false\n');
expect(resolved.output).toBe('superseded=false\nprovisional=false\n');
expect(resolved.stdout).toContain('::warning::');
expect(resolved.stdout).toContain('Could not read run 39237270547');
});
@@ -683,7 +701,7 @@ fi
// -gt coerces an empty operand to 0, so a field-less event would read
// as superseded and suppress every report.
expect(detect('2', { RUN_ATTEMPT: '' }).output).toBe(
'superseded=false\n',
'superseded=false\nprovisional=false\n',
);
});
it('gates every failure reader on the supersession check', () => {
@@ -751,6 +769,7 @@ fi
runAt = '2026-10-24T01:02:03Z',
markerIssues = [],
closedIssues = [],
provisional = false,
} = opts;
const tmp = mkdtempSync(join(tmpdir(), 'ci-issue-reuse-'));
try {
@@ -848,6 +867,7 @@ fi
RUN_ATTEMPT: '2',
SIGNATURE_TITLE: signatureTitle,
TEST_MARKERS: testMarkers,
PROVISIONAL: provisional ? 'true' : 'false',
RUN_AT: runAt,
LOOKUP_FAILURES: String(lookupFailures),
FAIL_COMMENT_ONCE: failCommentOnce ? '1' : '0',
@@ -905,6 +925,19 @@ fi
},
);
it('heads a provisional aggregate occurrence with the caveat', () => {
// The caveat is prepended once, ahead of the route split, so the
// aggregate's own body file is a third write site: a prepend narrowed to
// the per-signature branch would silently drop it here.
const result = run([botIssue(42)], { provisional: true });
expect(result.status).toBe(0);
expect(result.calls).toContain('issue comment 42');
expect(result.body).toMatch(
/^> \*\*Provisional\.\*\* Main advanced past this head/,
);
expect(result.body).toContain('Run 123 failed at commit abcdef123456');
});
it('retires the autofix route when adopting a legacy issue without the workflow marker', () => {
const result = run([botIssue(42, ' on abc123')], {
issueBody:
@@ -1386,6 +1419,18 @@ fi
'jq -r \'.searchMarkers[]?\' "${RUNNER_TEMP}/analysis.json"',
);
});
it('wires the provisional flag from the live step through to the writer', () => {
// Same chain shape, same failure mode: a retyped binding leaves
// PROVISIONAL empty, so every provisional filing silently loses its
// caveat; a dropped one is unbound and kills the writer outright.
expect(jobs.analyze.outputs.provisional).toBe(
'${{ steps.live.outputs.provisional }}',
);
expect(writer.env.PROVISIONAL).toBe(
'${{ needs.analyze.outputs.provisional }}',
);
});
describe('per-signature routing', () => {
const signatureTitle =
'Main CI failed: SDK Java — src/Foo.test > Suite … > case bar';
@@ -1420,6 +1465,37 @@ fi
expect(result.calls).not.toMatch(/ready-for-agent|autofix|assignee/);
});
it('leads a provisional filing with the caveat and still files it', () => {
// The successor had not settled when the report was prepared, so the
// issue is filed anyway; the caveat is what lets a reader close it
// when the newer run turns out to cover the failure.
const result = run([botIssue(42)], {
signatureTitle,
testMarkers: markerA,
provisional: true,
});
expect(result.status).toBe(0);
expect(result.body).toMatch(
/^> \*\*Provisional\.\*\* Main advanced past this head/,
);
expect(result.body).toContain('Run 123 failed at commit abcdef123456');
expect(result.body).toContain(`<!-- ${markerA} -->`);
// A provisional issue is still a human-triage unit, never autofix
// work — the whole reason reporting it is affordable.
expect(result.calls).toContain(
'issue edit 999 --repo QwenLM/qwen-code --add-label type/bug --add-label status/needs-triage',
);
expect(result.calls).not.toMatch(/ready-for-agent|autofix|assignee/);
});
it('omits the caveat when the tip was settled', () => {
const result = run([botIssue(42)], {
signatureTitle,
testMarkers: markerA,
});
expect(result.body).not.toContain('Provisional.');
});
it('comments on the issue whose body carries the test marker and accretes new ones', () => {
const tracked =
'Earlier report.\n\n<!-- qwen-main-ci-failure-test:aaa111bbb222 -->\n';
@@ -1466,6 +1542,27 @@ fi
expect(result.calls).not.toContain('issue edit');
});
it('heads a provisional recurrence comment with the caveat', () => {
// A recurrence is written from a separate file than the certified-miss
// create, so it is its own write site: scoping the prepend to the
// filing paths would leave a provisional recurrence unmarked.
const tracked = `<!-- ${markerA} -->\n<!-- main-ci-failure-scope:1234 -->\n`;
const result = run([botIssue(7, ' — Foo.test')], {
signatureTitle,
testMarkers: markerA,
issueBodies: { 7: tracked },
provisional: true,
});
expect(result.status).toBe(0);
expect(result.calls).toContain('issue comment 7');
expect(result.commentBody).toMatch(
/^> \*\*Provisional\.\*\* Main advanced past this head/,
);
expect(result.commentBody).toContain(
'Run 123 failed at commit abcdef123456',
);
});
it('keeps the recorded recurrence when the accretion edit fails', () => {
const result = run([botIssue(7, ' — Foo.test')], {
signatureTitle,
@@ -2003,6 +2100,11 @@ printf '%s\\n' '{"jobs":[]}'
expect(String(rerunJob.if)).toContain(
"needs.analyze.outputs.file_issue == 'true'",
);
// A provisional report is for a head main already replaced: the rerun
// would spend pool capacity on it and could not re-enter here.
expect(String(rerunJob.if)).toContain(
"needs.analyze.outputs.provisional != 'true'",
);
});
it('uses the same signature list as the PR-scan rerun of qwen-autofix.yml', () => {