Skip to content

fix(ci): merge-queue-triage retries delivery, and a refused post no longer destroys the triage - #9574

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9424-triage-comment-tolerance
Aug 18, 2026
Merged

fix(ci): merge-queue-triage retries delivery, and a refused post no longer destroys the triage#9574
os-steve merged 1 commit into
mainfrom
claude/issue-9424-triage-comment-tolerance

Conversation

@claude

@claudeclaudeBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes#9424

.github/workflows/merge-queue-triage.yml posted its entire deliverable through a bare
actions/github-script@v9 call with no retry and no handler. github-script routes any throw
from the inline script to main().catch(handleError)core.setFailed, so one transient
answer from the issues-comments endpoint killed the job — and took the diagnosis with it,
because body exists nowhere but the failed request. The run page was left showing
Unhandled error: HttpError and nothing else.

That loss is not the same as the sibling case. This comment is not a courtesy: it is the
machine-readable signal the PM dispatch loop keys on, and this file's own header says the
cross-PR flake evidence lives in these comments.

What changed

1. The retry policy is declared on the step, not hand-written.

retries: 3retry-exempt-status-codes: 400,401,404,422

actions/github-script has carried these two inputs since v6; they drive octokit's retry
plugin, which re-issues any request whose status is not exempt, plus network-level failures.
That is exactly the transient class the sibling ruling settled — 5xx, 429, secondary-rate-limit
403, network codes — with no classifier of my own to maintain. 403 is removed from the
action's default exempt list
(400,401,403,404,422) deliberately: GitHub answers a secondary
rate limit with 403 as well as 429, and this job is rate-limit-shaped (it paginates the run's
jobs, pulls up to four job logs, then paginates 24 h of merge_group runs). The price is that a
genuine permission denial now takes four attempts to fail instead of one. It still fails.

Nothing is swallowed by this: a spent retry still throws. Detection failures fail exactly as
before
— the job harvest and the 24 h history are above the handler and still take the job
red, with Unhandled error: unchanged.

2. A refused delivery no longer destroys the triage. The computed comment is written into
the run's job summary — where it outlives the request, is readable without a re-run, and can be
pasted onto the PR by hand — with a line telling the reader how to tell platform weather (5xx /
429 / network, re-run it) from this repo's own bug (422 over-long body, 403 permissions, re-run
will not help).

3. And then it still fails. This is a deliberate divergence from PR #9423, which kept the
docs-drift job green. There, the job's conclusion is a check on the PR and its comment is
advisory, so a red costs a reader's attention for nothing. Here the job is workflow_run: its
conclusion appears on no check list, is in no required context (verified against
scripts/check-required-contexts.mjs — this workflow has no entry) and gates nothing. So red
costs nothing, while a green that quietly delivered nothing would recreate this card's silence
one layer further in. Failing buys a total invariant something can be keyed on:

this job is green the triage comment is on the PR

A non-fatal warning was the shape the dispatch suggested. It was not taken because a warning
annotation lives only on a workflow_run run page nobody is watching, whereas a failed run is
durable, queryable (gh run list --workflow "Merge Queue Triage" --status failure) and
notifying. If the maintainer prefers uniformity with #9423 over the invariant, the change is
one line — core.setFailed to core.warning.

4. The de-duplication listing degrades to at-least-once. Its marker is scoped to one run
id, so a duplicate is inert (same run, same text, self-evident pair) while a miss loses the
signal entirely. PR #9423 chose the opposite for docs-drift, and correctly: that marker is
stable across runs, so posting blind there strands a second advisory the dedup then updates
forever alongside the first.

Dispatch assumptions, tested

  • "The card's grep is still true." Half falsified. continue-on-error is still 0, and the
    comment post is genuinely unguarded — but the card's catch (error)\|catch (err) pattern
    reported 0 catches while the file has one: catch (e) at the job-log harvest. It does not
    cover the comment post, so the finding stands; the measurement quoted on the card does not.
  • "The two workflows' comment paths are identical." They are not, in three ways that
    changed the fix: this one only ever creates (docs-drift updates-or-creates); its marker is
    per-run, not stable; and it makes four other API calls before the post, all of them
    detection, which is why a blanket step-level tolerance was never an option here.
  • "The tolerance code from ci(docs-drift): tolerate transient API failures when delivering the advisory, loudly #9423 is reusable as-is." It is, but copying it was the wrong
    move and so was extracting it. merge-queue-triage.yml is a workflow_run job that
    deliberately never checks out (stated in its header), so a shared module would have meant
    adding actions/checkout to a privileged-context job and inventing a scripts/ci/ pattern
    the repo does not have, to share ~30 lines between two call sites. Using the action's own
    declared inputs removes the duplicated rule instead of relocating it: there is no second
    copy of the classifier, because this file writes none.

Validation — simulated, and stated as such

No real 503 was observed and none is claimed; these error branches cannot be triggered on
demand in real CI. Following the technique PR #9524 landed: a python yaml parser extracts the
shipped script — and the shipped retries / retry-exempt-status-codes values — from this
workflow file, and a harness executes it under a stubbed octokit and @actions/core,
reproducing github-script's own main().catch(handleError) propagation and octokit's retry
semantics driven by the declared inputs. The harness is deliberately not in this diff: the
scaffolding is validation, not a product.

Baseline (origin/main, which declares no retries) reproduces the card's exact signature:
create-503-once and create-503-persistent both →
FAILURE: Unhandled error: HttpError: No server is currently available to service your request.,
comment not posted, nothing preserved anywhere.

After, 14/14 scenarios as designed:

scenariooutcomecommentdiagnosis preservedcreateComment attempts
happySUCCESSposted1
create-503-onceSUCCESSposted2
create-503-persistentFAILUREyes (2298 B)4
create-429-persistentFAILUREyes4
create-ECONNRESET-persistentFAILUREyes4
create-403-secondary-ratelimitSUCCESSposted3
create-403-plainFAILUREyes4
create-422-body-too-longFAILUREyes1 (exempt, not retried)
list-503-persistentSUCCESSposted1 (+1 warning)
already-postedSUCCESSskipped
detect-jobs-500-persistentFAILURE (Unhandled error:)
detect-runs-503-persistentFAILURE (Unhandled error:)
logs-410SUCCESSposted, names only1
not-a-queue-branchSUCCESSnone

The load-bearing rows are the two detect-* ones (tolerance never reaches detection),
create-422 and create-403-plain (this repo's own bugs still fail), and list-503-persistent
(where the baseline lost the comment entirely and this now delivers it).

Gates

Union re-run after the final commit 9647f89cd, all exit 0: pnpm check:node-version,
pnpm check:required-contexts, pnpm check:shard-attestation,
pnpm check:workflow-status-functions, pnpm check:nul-bytes, node scripts/check-shard-attestation.mjs. Re-derivation with
node scripts/pm/dispatch-gates.mjs on the actual changed path added no family beyond the
dispatched list. Control-character self-scan on the changed file: clean.

Workflow-only change, nothing published — skip-changeset.


Generated by Claude Code

…onger destroys the triage
The triage comment is the machine-readable signal the PM dispatch loop keys on,
and the cross-PR flake evidence lives in it — but it was posted through a bare
`github-script` call with no retry and no handler, so a transient GitHub API
failure killed the job and took the diagnosis with it: `body` exists nowhere but
the failed request, and the run page was left showing `Unhandled error:
HttpError` and nothing else.
Declare the retry policy on the step (`retries: 3`, with 403 removed from the
exempt list because GitHub answers a secondary rate limit with 403 as well as
429, and this job paginates jobs, logs and 24 h of runs). Nothing is swallowed:
a spent retry still throws, so detection failures fail exactly as before.
On a refused delivery, write the whole computed triage into the job summary —
where it outlives the request and can be pasted onto the PR — and then fail.
Failing is deliberate and diverges from the sibling fix in docs-drift-check.yml
(#9373/#9423): this is a `workflow_run` job whose conclusion is on no check
list, in no required context and gates nothing, so red costs nothing while a
green that quietly delivered nothing recreates the silence one layer further
in. That buys the invariant: green ⇔ the triage comment is on the PR.
The de-duplication listing degrades to at-least-once instead: its marker is
scoped to one run id, so a duplicate is inert while a miss loses the signal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

merge-queue-triage.yml has the same untolerated comment-post as #9373 — and here the lost comment IS the flake evidence

2 participants

@os-steve@claude