fix(#2086): gate each dev-lead retry payload on the target repo's pinned parser
Problem
The dev-lead retry sweep runs scripts/ at a single channel: the one this repo's dev-lead.yml pins (#2050 / #2083). It then dispatches retries to every repo in the org, and each target parses the payload with dev-lead-intent.sh at its own pin. As a result, a target pinned to an older channel can receive a client_payload field its parser drops. That is the #2017 / #2050 skew, now between repos instead of over time.
The concrete case is comment_node_id: the v1 parsers don't read it, so a fix-bot-comment retry sent to a v1-stable repo would misroute.
Change
The first version of this PR skipped whole repos pinned older than the sweep. That would have stopped all retries for the fleet (stable / v1-stable), even though most payloads only carry fields every release reads. This version gates each payload separately instead.
scripts/lib/dispatch-channel-gate.sh works per target:
- It finds the target's pin: the
dev-lead.ymlagent_ref, or the reusable's defaultmainwhen there is none. - It reads the fields that pin's
dev-lead-intent.shreads: every.client_payload.<key>in the parser and in thelib/*.shfiles it names, followed transitively, with comments stripped. This is the same readingcheck-retry-payload-fields.shdoes in CI. - Both lookups are cached per sweep.
The payload is sent only when the target reads every non-informational field. Informational fields are the same ones the CI check uses (repo, attempt).
- If the target pins the same channel as the sweep, no lookup is needed.
- If the target has no
dev-lead.yml(a 404 on a repo the token can read), the payload is held with no warning. - If the pin, the repo or the parser can't be read, the payload is held with a
::warning::(fail closed). - If a field isn't read, the payload is held with one
::warning::per repo and field set, naming the field.
Where the gate runs:
- Every dispatcher calls
dcg_payload_allowedon the payload it built. A new test enforces this for any function that POSTs to/dispatches. - The bot-comment scan checks
BOT_COMMENT_RETRY_FIELDSbefore posting its retry marker. Otherwise a marker would block the comment for the whole pending window. A test pins this list to the dispatcher's payload. main()no longer skips repos. It turns the gate on fromSWEEP_AGENT_REF, whichdev-lead-retry.ymlnow passes from its resolved channel.- An invalid
SWEEP_AGENT_REFfails the sweep. - With
SWEEP_AGENT_REFunset, as in unit tests orcheck-retry-payload-fields.shsourcing the sweep, the gate is off and logs that it is.
- An invalid
Risk
Low to medium. The change only removes dispatches. A held payload leaves its markers in place, so the retry resumes once the target is repinned. Today the only held case is fix-bot-comment to repos whose pinned parser doesn't read comment_node_id. The extra API reads are cached: one stub read per repo, and a few file reads per distinct pin.
Test plan
shellcheck --severity=warning scripts/*.sh scripts/lib/*.sh: clean.bash scripts/dev-lead-lint.sh: clean.bash scripts/check-retry-payload-fields.sh: OK at v139-next, ring0, ring1 and stable. The new lib introduces no dispatcher and nodispatchesglobal.tests/dev-lead/unit/test_dispatch_channel_gate.bats: 27/27. The mockghfails on any unexpected call.tests/dev-lead/unit/test_bot_comment_retry.bats: 67/67, including an end-to-end test where the target dropscomment_node_id, so no marker is posted and nothing is dispatched.- Mutation checks: removing the early bot-comment gate, or any dispatcher's gate, fails a test.
- These suites also pass:
test_retry_payload_fields,test_delayed_rate_limit_retry,caller_stub_freeze, andtest_{auto_rebase_retry,bot_thread_retry,dev_lead_retry,dev_lead_retry_channel,dev_lead_retry_pr_escalation,hold_notice,channel_stub_forward_guardrail}.
Rollback
Revert this PR. There are no non-revertible side effects.
Monitoring
The dev-lead-retry run logs [retry] sweep channel: dev-lead/v…. A held payload logs ::warning::[retry] holding retries for <repo>: it pins <ref>, whose scripts/dev-lead-intent.sh does not read client_payload field(s) … and [hold] <repo> ….
Closes #2086
🤖 Generated with Claude Code