Skip to content

fix(ci): report why a pr-unblock select job could not read the pull request - #13117

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/resolve-conflicts-select-read-error
Nov 4, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
fix/resolve-conflicts-select-read-error

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: hub ledger item for pr-unblock (06:3xZ); a select job swallowed its pull-request read and reported "the PR names no head SHA" (#13027, run 41775375216).

Summary

Four pr-unblock select jobs read the pull request with || true, so a failed read surfaced only as "the PR names no head SHA". On #13027 (run 41775375216, job 126394422464, 04:48Z) the read actually failed with invalid character 'b' looking for beginning of value, a non-JSON body, not a missing SHA and not a rate-limit refusal.

Fix

  • New .github/scripts/pr-unblock/read-pr-head.bash, read_pr_fields <number> <jq fields>: reads the pull request and classifies a failure. It reads at most twice:
    • rate-limit (rate limit, abuse detection, HTTP 429, the wrapper's "was not sent", or api_budget_stop_until core set): never retried.
    • transient (5xx, invalid character, unexpected end of JSON input, connection or timeout): read once more after 5-10 s.
    • other: fails at once.
    • The step fails with ::error::the PR could not be read (<class>): <text>, single-line, capped at 300 characters, % escaped.
  • pr-unblock.yml: resolve-conflicts-select, sync-shared-copies-select, address-feedback-select and review-dependency-select source the helper from .base (the default-branch tip each already checks out, never head code) after their unchanged api_budget_wrap_gh line. A read that succeeds with no SHA still reports "the PR names no head SHA"; exit codes are unchanged.

Verification

  • workflow-gates.test.mjs: 54/54. It includes a classification test of the sourced helper with a stub gh and sleep covering:

    • transient then success, for 4 error texts;
    • transient twice;
    • a sanitized 404;
    • 4 rate-limit forms, each with exactly 1 call;
    • empty error text;
    • a real "no head SHA".

    A parameterized test also checks that all four steps source the helper from .base.

  • api-budget-coverage.test.mjs 5/5; merge-holds.test.mjs 65/65.

  • scripts/check-affected.sh --base origin/main --no-fetch: exit 0.

  • Left out: the cause of the non-JSON body (no HTTP status in the log); the transient retry covers it.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 5 commits November 4, 2026 01:24
…request

The Read the PR head step swallowed a failed `gh api` with `|| true`, so any
read failure reported "the PR names no head SHA". Run 41775375216 (#13027)
failed that way on `invalid character 'b' looking for beginning of value`,
a non-JSON body from the API that the step never showed.

The step now keeps the read's stderr and fails with its text and class:
transient (5xx, timeout, connection error, non-JSON body), rate-limit (the
api-budget wrapper's refusal included) or other. A transient failure is read
once more after 5 to 10 s; a rate limit and any other failure are not read
again. A read that succeeds with no head SHA still reports as before.

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

The same swallowed `gh api` read (`|| true`, then "the PR names no head SHA")
sat in four steps: resolve-conflicts-select, sync-shared-copies-select,
address-feedback-select and review-dependency-select. Each already checks out
the default branch tip at `.base`, so the read, its failure classes and its one
transient retry move to `.github/scripts/pr-unblock/read-pr-head.bash`
(`read_pr_fields`), sourced from `.base` in all four. The file joins the
enforcement hold floor like the other scripts those jobs run with a token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api-budget-coverage.test.mjs treats a script path named in ci-status's
hold_patterns as a script that step runs, and the helper's gh call is wrapped
by its sourcing step, not by itself. Drop the hold-floor rows added for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 4, 2026 06:57
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 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-11-04T06:59:14.758790Z 51c4fa4 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 Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed ece8bd8f8082accfdd40439d76b6fb64cb87e150: read 1 of 1 changed file(s); 0 finding(s), 0 as inline comments.

Security review (incremental since 51c4fa4): no security issues found

I read the whole incremental change. It touches one file, .github/actions/resolve-config/callers.test.mjs, in one hunk.

What changed: the pr-unblock.yml: resolve-conflicts-select skips a PR whose base is not the default branch test now builds a work/ directory. Inside it, it symlinks .base to the repository root and runs the step with cwd: work. The step now sources read-pr-head.bash from .base, so the test needs this layout to reach the helper.

What I checked:

  • Trusted path. The file is on a trusted path (.github/actions/resolve-config/**). It is a test of resolve-config, not the control itself: resolve-config's tests, fixtures and docs are outside the token-broker control. Nothing in resolve.mjs, its sibling modules, effect-grants.json, lane-rules.json or activity-rules.json changed. So no token scope, lane, activity effect or session allowlist widened.
  • What runs. The change only moves the working directory of a local test run and adds a symlink inside a temp directory. It still uses the stand-in gh, so no real credentials or network calls are involved. No workflow, gate, merge hold, trusted-actor list or kill switch changed.
  • The PR description. It contains no embedded instructions aimed at the review.

critical-findings: 0

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed ece8bd8f8082accfdd40439d76b6fb64cb87e150: read 1 of 1 changed file(s); 0 finding(s), 0 as inline comments.

Code review (incremental, since 51c4fa4): PR #13117

No findings. I read the one hunk this push changed, in .github/actions/resolve-config/callers.test.mjs (the only file on the incremental list). The file is on the trusted-path list (.github/actions/resolve-config/**), so I also read the step the test runs (pr-unblock.yml L240-L246) and the api-budget wrapper that step now sources.

What I checked

  • The fix matches the earlier change. Since the last review, the Read the PR head step sources .base/.github/scripts/pr-unblock/read-pr-head.bash. Before this push, the test ran the step with no .base directory under its working directory, so read_pr_fields would not exist. The test now makes a work/ directory, links work/.base to the repository root, and runs the step with cwd: work. new URL("../../..", import.meta.url) from .github/actions/resolve-config/ resolves to the repository root, so .base/.github/... points at this checkout's own helper. mkdirSync, symlinkSync and fileURLToPath are already imported (callers.test.mjs#L14-L17).
  • The wrapper is now active in the test, and this is safe. Because .base now exists, api-budget.bash really loads and api_budget_wrap_gh defines a gh function before, || true hid this. I checked that the stub gh still works under it. The wrapper adds -i and saves the output. When the first line is not an HTTP/x NNN status line, api_budget_read prints the body unchanged (api-budget.bash#L131, L145). So the three lines the stub prints still reach read_pr_fields. The stub writes no * Request to debug lines and no header block, so it records no reading in the runner's $RUNNER_TEMP/api-budget state.
  • Cleanup. A recursive delete of the temp directory removes the .base link, not the repository it points to.

I did not run the tests. That the suite passes is the author's claim and I have not checked it.

Other notes

  • There is no closing issue (the PR body says so), so there were no acceptance criteria to check.
  • The PR context file and the prior-findings file contain no embedded instructions aimed at this lane. The prior round had 0 findings, so there is nothing to repeat, and the round cap does not apply.
  • Spend: none. This push changes only a test.

resolve-conflicts-select's head step now sources read-pr-head.bash from the
default-branch checkout at .base, and the callers test ran the step in a
directory with none. Run it in a work directory whose .base links to the
repository root, the layout the job's checkout gives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit c1d9cea Nov 4, 2026
46 checks passed
@kyle-sexton
kyle-sexton deleted the fix/resolve-conflicts-select-read-error branch November 4, 2026 07:17
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