Skip to content

fix(pr-merge): link a refused run to its PR by the check run's external id - #10684

Merged
kyle-sexton merged 10 commits into
mainfrom
fix/refire-refused-match-run
Oct 28, 2026
Merged

kyle-sexton merged 10 commits into
mainfrom
fix/refire-refused-match-run

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Refs: #10583

Summary

pr-merge's scheduled step "Re-fire lane runs refused as workflow-modified" never re-fired a run, because it linked a refused run to its PR by a check run's details_url, and GitHub does not return the details_url this repository posts. Check run 122864732692 ("pr-unblock / apply-feedback-patch" on #10583, posted by run 40700900804, which the broker refused as workflow-modified) reads back with details_url: https://github.com/melodic-software/claude-code-plugins/runs/122864732692 and external_id: "", though report-check-run POSTed .../actions/runs/40700900804. The scheduled run 40711059681 therefore printed nothing for #10583.

GitHub's Create a check run docs say only: "details_url (string) The URL of the integrator's site that has the full details of the check. If the integrator does not provide this, then the homepage of the GitHub app is used." and "external_id (string) A reference for the run on the integrator's system." They say nothing about a GitHub Actions token's details_url being replaced; the basis for that is the observed check run above. external_id set over the API is kept: every pr-merge / gate check reads back with external_id: pr-merge-gate.

fix-ci-dispatch.mjs parsed the same field (RUN_PAGE), so on live data it never paired a fix-ci check with its run's lane-stopped stop and always took the timing fallback.

Fix

  • report-check-run sets external_id to <lane>:<activity>:<run id>:<run attempt> on every check it posts. Before, it set it only when the run job left a lane record, so a run that failed before the skill (the lane-token-denied case) posted none. The format is the one docs/conventions/pr-pipeline/pr-run-activity.md already specified; no other reader parses it.
  • New .github/actions/report-check-run/posting-run.mjs owns the format: externalIdOf builds it and postingRunId(check) reads the run id back from it, falling back to a details_url that names the run's own /actions/runs/<id> page. refire-refused.mjs and fix-ci-dispatch.mjs both use postingRunId. lane-record.mjs no longer builds the id.
  • The check run's own /runs/<check-run-id> page carries no run id, so the fallback cannot read it.
  • README, action.yml and pr-run-activity.md describe the new behavior. Changelog fragment: .changes/pr-pipeline/.

Behavior changes to note:

  • The report-check-run tests "no lane record POSTs no ... external_id" and the contract-body key list change on purpose: every posted check now carries external_id.
  • fix-ci-dispatch now pairs a fix-ci and fix-ci-rung-up check by run, as its comment always said, instead of treating each check as its own attempt.

Existing PRs: check runs posted before this merges have an empty external_id and a /runs/<check-run-id> details_url, so neither path links them. #10583's refused round will not be re-fired automatically; it needs a new head (any push) once this lands. No migration is needed.

Verification

  • Fail-before: with the test changes alone (live-shape fixtures: details_url /runs/<check-run-id>, external_id naming the run), node --test over refire-refused, fix-ci-dispatch and report-check-run tests gave tests 127, pass 109, fail 18, including the new "a refused run links to its PR through the check's external_id ..." and "a run job that failed before the skill names its run in external_id".
  • After the fix: refire-refused, fix-ci-dispatch, every report-check-run test file (posting-run included) and dispatch tests: tests 260, pass 260, fail 0; on the head merged with main: tests 197, pass 197, fail 0.
  • bash scripts/check-affected.sh --tests: 11 passed, 5 skipped over 14 changed files.
  • uvx pytest plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py: 13 passed, 503 subtests passed (no floor row needed).

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 28, 2026 12:14
…al id

GitHub returns a check run a GITHUB_TOKEN posts with details_url set to
the check run's own /runs/<check-run-id> page, whatever the POST set, so
refire-refused never matched a refused run to its PR, and fix-ci-dispatch
never grouped a fix-ci pair by run.

report-check-run now sets external_id <lane>:<activity>:<run>:<attempt>
on every check it posts, not only those with a lane record, and
posting-run.mjs reads the run back from it, falling back to a details_url
that names the run's page. Both readers use that one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nal id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 28, 2026 16:32
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T16:33:59.696257Z 018f38e Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed fa43df395b5a4f79c81f174a2defd9474f2873fd: 0 finding(s), 0 as inline comments.

Incremental review of the changes since 4547b27: posting-run.mjs, its test and README, and fix-ci-dispatch.mjs with its test. I found nothing to flag.

  • EXTERNAL_ID regex: the attempt is now a second capture group. Group 1 is still the run id, so postingRunId and refire-refused.mjs behave as before.
  • postingAttempt: it reads <run id>:<attempt> from external_id only. A details_url run page returns null, as it should, because that page names no attempt.
  • withStops: it now groups checks and matches stops by run attempt. A fix-ci check and its in-run rung-up check share a run attempt, so they still pair as one attempt. A stop in attempt 1 no longer marks a successful re-run in attempt 2. Checks with no readable attempt, or where either side lacks one, still use the timing fallback.
  • Tests: the new test covers both cases. A stop in attempt 1 with a successful attempt 2 leaves the head handled. A stop in the same attempt still lets the head be retried.
  • Docs: the README and code comments match the new behavior.

I read the code and traced the logic. I did not run the tests, so the PR's own test counts are author-claimed and unverified.

No spend or hold applies.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed fa43df395b5a4f79c81f174a2defd9474f2873fd: 0 finding(s), 0 as inline comments.

I found no security issues in the incremental change since 4547b27.

  • What changed: posting-run.mjs now captures the run attempt from external_id and exposes postingAttempt (<run id>:<run attempt>). fix-ci-dispatch.mjs uses it in withStops to pair a lane-stopped apply-patch check with a fix-ci check by run attempt instead of by run id. The README and tests were updated to match.
  • Who can reach it: both check lists in fix-ci-dispatch.mjs come from api.listCheckRuns({ ..., appId: actionsAppId }) (lines 565-566), and the rollup path filters on checkSuite.app.databaseId === actionsAppId (line 150). Only checks posted through the Actions app's own token count, so an outside author cannot forge a stop or an attempt id.
  • What the change does to the retry cap: it narrows which attempt a stop marks. A stop in attempt 1 no longer marks a re-run's attempt 2. The per-head cap still counts spent per check (capCount with maxRunsPerHead, lines 428 and 581), so retries stay bounded. The only effect is on retry and spend behavior, with no token, trust or merge-hold impact. The \d{1,20} bounds on both captures are unchanged, and the regexes are anchored, so there is no injection or ReDoS surface.
  • Fallback: a check with no readable attempt still falls back to timing, as before.
  • Spend: none. The change only affects whether a head gets a re-run under the existing caps, which draws on prepaid or subscription usage.

manifest: not hit, since this diff touches no plugin manifests.
critical-findings: 0

kyle-sexton and others added 3 commits October 28, 2026 12:37
refire-refused.mjs, a floored gate script, now loads
.github/actions/report-check-run/posting-run.mjs, so that module joins
the floor as `gate` in every copy. The floor-closure test now scans
refire-refused.mjs's imports, which is how it found the gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…floor row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 28, 2026
@melodic-automation-lanes melodic-automation-lanes Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 28, 2026
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

The pr-unblock lane stopped on this pull request and needs a person: a conflicted path is a workflow, submodule config or a settings file, which the lane does not resolve.

  • Reason: protected-path-conflict
  • Resume key: db7de74183b0c8f3df1506899f0f9ec77fdf5653 (the head the lane stopped on)

To resume, deal with the reason above, remove the needs-human label (and do-not-merge, if set), then dispatch the pr-unblock workflow with pr-number=10684 and activity=resolve-conflicts:

gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=10684 -f activity=resolve-conflicts

…tch-run

# Conflicts:
#	.github/scripts/pr-merge/merge-holds.mjs
#	.github/workflows/pr-require-checks.yml
#	.github/workflows/pr-review-hosted.yml
#	.github/workflows/pr-review-security-hosted.yml
#	plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py
#	plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py
#	plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on 4547b2763e210a78751f637e0851743936ae97d0; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/scripts/pr-unblock/fix-ci-dispatch.mjs Outdated
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

The pr-unblock lane escalated this pull request as protected-path-conflict, and that no longer holds at 4547b2763e210a78751f637e0851743936ae97d0: GitHub reports no conflict with main. pr-merge removed needs-human, so the lanes run on it again. To keep a person on it, add needs-human back: a label a person adds is never removed.

@melodic-automation-lanes melodic-automation-lanes Bot removed the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 28, 2026
kyle-sexton and others added 2 commits October 28, 2026 13:58
A re-run is the same run id at its next attempt, so keying withStops on
postingRunId let a lane stop in attempt 1 mark the attempt 2 that fixed
the head, and the head was re-dispatched. postingAttempt reads the run id
and attempt from external_id; withStops groups checks and matches stops
on it, and a check with no readable attempt falls back to timing as an
unknown run id did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 48a2b13 Oct 28, 2026
49 checks passed
@kyle-sexton
kyle-sexton deleted the fix/refire-refused-match-run branch October 28, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant