fix(claude-ci-fix): resolve PR via API when check_run payload is empty#157
fix(claude-ci-fix): resolve PR via API when check_run payload is empty#157
Conversation
- Remove pull_requests[0] != null guard from if condition; GitHub
frequently omits this array in check_run webhook payloads for
external checks (SonarCloud, CodeQL, etc.)
- Add Resolve PR number step that falls back to the commits/{sha}/pulls
API when the payload's pull_requests array is empty
- Fix self-exclusion name filter: was 'claude-code / claude' (wrong
case); actual check run names start with 'Claude Code'
- Fix concurrency key: was referencing pull_requests[0].number which
is null when payload is empty; now uses head_sha
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 47 minutes and 38 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughModified Changes
Sequence Diagram(s)sequenceDiagram
participant GitHub as GitHub Event
participant Workflow as Workflow Engine
participant ResolvePR as Resolve PR Number
participant GH_API as GitHub API
participant Checkout as Checkout Repository
participant RunCode as Run Claude Code
GitHub->>Workflow: Trigger check_run event
Workflow->>ResolvePR: Execute step
ResolvePR->>ResolvePR: Check pull_requests[0].number
alt PR number available
ResolvePR->>ResolvePR: Use PR number
else PR number empty
ResolvePR->>GH_API: Query for open PRs by head_sha
GH_API-->>ResolvePR: Return PR number
end
ResolvePR-->>Workflow: Output PR number
alt PR number resolved
Workflow->>Checkout: Proceed (gated on PR number)
Checkout->>Checkout: Checkout repository
Workflow->>RunCode: Proceed (gated on PR number)
RunCode->>RunCode: Run Claude Code
else PR number not resolved
Workflow->>Workflow: Skip downstream steps
end
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/claude-code-reusable.yml:
- Around line 58-60: The workflow currently keys concurrency on
github.event.check_run.head_sha which prevents coalescing runs for the same PR
and can cause duplicate Claude fix runs; implement a two-job split: add a
lightweight resolve-pr job (use github.event.check_run.pull_requests[0].number
and fallback to querying the repo commits/<head_sha>/pulls) that emits an output
like number, then change the claude-ci-fix job to depend on needs.resolve-pr and
set concurrency.group to claude-ci-fix-${{ needs.resolve-pr.outputs.number }}
with cancel-in-progress: true, falling back to head_sha only when the resolve-pr
output is empty (use the resolve job output to decide the concurrency key).
- Around line 75-82: The script treats check_run.pull_requests[0].number as
authoritative but only filters for open PRs in the fallback gh api path
(select(.state == "open")), causing asymmetric behavior; update the fast path
that uses PR="${{ github.event.check_run.pull_requests[0].number }}" to validate
the PR state via the GitHub API (or call gh api repos/.../pulls/$PR and check
.state == "open") before assigning PR, so both the direct-payload and fallback
branches perform the same "open" check and only set/echo number=$PR when the PR
is still open.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 83854682-2f86-4269-b38f-ffbfcea0cf3d
📒 Files selected for processing (1)
.github/workflows/claude-code-reusable.yml
| concurrency: | ||
| group: ${{ github.event.check_run.pull_requests[0] && format('claude-ci-fix-pr-{0}', github.event.check_run.pull_requests[0].number) || format('claude-ci-fix-run-{0}', github.run_id) }} | ||
| group: claude-ci-fix-${{ github.event.check_run.head_sha }} | ||
| cancel-in-progress: true |
There was a problem hiding this comment.
Concurrency now keyed per-commit — enables parallel Claude runs on the same PR.
Switching from pull_requests[0].number to head_sha fixes the null-key bug, but removes per-PR coalescing. If two failing checks report against different SHAs of the same PR (slow external checks after a push, or multiple pushes in quick succession), both workflows land in distinct concurrency groups, cancel-in-progress does nothing, and two Claude instances will concurrently check out the PR branch and push fixes — producing push races and duplicate/conflicting fix commits.
Consider resolving the PR number before concurrency evaluation and keying on it when available. Since workflow-level concurrency cannot depend on step outputs, one option is to move PR resolution into a lightweight preceding job and use needs.<job>.outputs.pr for the fix job's concurrency group, falling back to head_sha only when no PR is found:
♻️ Sketch: two-job split so concurrency can key on PR number
jobs:
resolve-pr:
if: github.event_name == 'check_run' && github.event.check_run.conclusion == 'failure' &&
!startsWith(github.event.check_run.name, 'Claude Code')
runs-on: ubuntu-latest
outputs:
number: ${{ steps.pr.outputs.number }}
steps:
- id: pr
env:
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS || github.token }}
run: |
PR="${{ github.event.check_run.pull_requests[0].number }}"
if [ -z "$PR" ]; then
PR=$(gh api \
"repos/${{ github.repository }}/commits/${{ github.event.check_run.head_sha }}/pulls" \
--jq '[.[] | select(.state == "open")] | first | .number // empty')
fi
echo "number=$PR" >> "$GITHUB_OUTPUT"
claude-ci-fix:
needs: resolve-pr
if: needs.resolve-pr.outputs.number != ''
concurrency:
group: claude-ci-fix-${{ needs.resolve-pr.outputs.number }}
cancel-in-progress: true
...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/claude-code-reusable.yml around lines 58 - 60, The
workflow currently keys concurrency on github.event.check_run.head_sha which
prevents coalescing runs for the same PR and can cause duplicate Claude fix
runs; implement a two-job split: add a lightweight resolve-pr job (use
github.event.check_run.pull_requests[0].number and fallback to querying the repo
commits/<head_sha>/pulls) that emits an output like number, then change the
claude-ci-fix job to depend on needs.resolve-pr and set concurrency.group to
claude-ci-fix-${{ needs.resolve-pr.outputs.number }} with cancel-in-progress:
true, falling back to head_sha only when the resolve-pr output is empty (use the
resolve job output to decide the concurrency key).
| run: | | ||
| PR="${{ github.event.check_run.pull_requests[0].number }}" | ||
| if [ -z "$PR" ]; then | ||
| PR=$(gh api \ | ||
| "repos/${{ github.repository }}/commits/${{ github.event.check_run.head_sha }}/pulls" \ | ||
| --jq '[.[] | select(.state == "open")] | first | .number // empty') | ||
| fi | ||
| echo "number=$PR" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
PR state not verified on the fast path — asymmetric with the API fallback.
When check_run.pull_requests[0].number is present in the payload, the script accepts it as-is; when falling back to the API, only open PRs are kept (select(.state == "open")). A failing check_run that arrives after the PR was merged/closed will trigger Claude on the fast path but skip on the fallback path. Consider treating the payload number as a hint and validating state uniformly, e.g.:
🛡️ Proposed fix
run: |
PR="${{ github.event.check_run.pull_requests[0].number }}"
- if [ -z "$PR" ]; then
- PR=$(gh api \
- "repos/${{ github.repository }}/commits/${{ github.event.check_run.head_sha }}/pulls" \
- --jq '[.[] | select(.state == "open")] | first | .number // empty')
- fi
+ if [ -n "$PR" ]; then
+ STATE=$(gh api "repos/${{ github.repository }}/pulls/$PR" --jq '.state' 2>/dev/null || echo "")
+ [ "$STATE" = "open" ] || PR=""
+ fi
+ if [ -z "$PR" ]; then
+ PR=$(gh api \
+ "repos/${{ github.repository }}/commits/${{ github.event.check_run.head_sha }}/pulls" \
+ --jq '[.[] | select(.state == "open")] | first | .number // empty')
+ fi
echo "number=$PR" >> "$GITHUB_OUTPUT"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/claude-code-reusable.yml around lines 75 - 82, The script
treats check_run.pull_requests[0].number as authoritative but only filters for
open PRs in the fallback gh api path (select(.state == "open")), causing
asymmetric behavior; update the fast path that uses PR="${{
github.event.check_run.pull_requests[0].number }}" to validate the PR state via
the GitHub API (or call gh api repos/.../pulls/$PR and check .state == "open")
before assigning PR, so both the direct-payload and fallback branches perform
the same "open" check and only set/echo number=$PR when the PR is still open.
- Document the third job (claude-ci-fix) in ci-standards.md section 4: update jobs list, triggers example, and checkout requirement note - Extend check_claude_workflow_checkout() to also verify the check_run trigger is present — without it claude-ci-fix can never fire
|



Problem
The
claude-ci-fixjob was not triggering on CI failures. Three bugs combined to prevent it:check_run.pull_requestsis often empty — GitHub's webhook payload frequently omits thepull_requestsarray for external checks (SonarCloud, CodeQL, status checks). Thepull_requests[0] != nullguard in theifcondition silently skipped every such event.Self-exclusion name filter had wrong case — the filter used
'claude-code / claude'(lowercase) but GitHub names check runs using the workflow'sname:field, which is"Claude Code". The filter never matched, meaning Claude's own check runs could re-trigger the fix job.Concurrency key referenced
pull_requests[0].number— evaluated at workflow scheduling time, before the job runs. When the payload array is empty this produces a null key, breaking concurrency deduplication.Fix
pull_requests[0] != nullguard from theifconditionResolve PR numberstep that uses the payload value when present, and falls back to the/commits/{sha}/pullsAPI endpoint when notsteps.pr.outputs.number != ''!startsWith(..., 'Claude Code')head_shaSummary by CodeRabbit