Skip to content

Commit 04827a1

Browse files
ci: fix Claude auto-review auth (use github_token, skip OIDC exchange) (#8)
* ci: fix Claude auto-review auth (use github_token, skip OIDC exchange) The pull_request_target review 401s exchanging its OIDC token for a GitHub App token. Supplying github_token makes the action use it directly and skip that exchange. Drops the now-unneeded id-token: write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add cost gate: skip paid Claude review if sibling CI checks failed Poll the PR head's check-runs before the paid review and only run it when all sibling checks are clean (or none exist after a short grace window). Excludes this workflow's own check-run (claude-review) and treats a poll timeout as skip. Uniform across repos: sibling checks are discovered dynamically, no per-repo workflow-name hardcoding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 686f5ad commit 04827a1

1 file changed

Lines changed: 50 additions & 2 deletions

File tree

.github/workflows/claude-code-review.yml

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,55 @@ jobs:
2727
github.event.pull_request.draft == false &&
2828
github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade'
2929
runs-on: ubuntu-latest
30-
timeout-minutes: 30
30+
timeout-minutes: 60
3131
permissions:
3232
contents: read
3333
pull-requests: write # post the review comments
34-
id-token: write
34+
checks: read # read sibling check-runs for the cost gate
3535
steps:
36+
# COST GATE: the paid Claude review is the last thing to run. Wait for the
37+
# PR head's OTHER check-runs to finish and only proceed if they are clean.
38+
# If any sibling check failed we skip the review to avoid spending money
39+
# reviewing a PR that is already known-broken. Uniform across all repos:
40+
# it discovers sibling checks dynamically (no per-repo workflow names).
41+
# - decision=run : all sibling checks completed with a good conclusion,
42+
# OR no sibling checks exist after a short grace window
43+
# (nothing to gate on), OR the poll timed out is treated
44+
# as skip (see below).
45+
# - decision=skip : at least one sibling check failed/cancelled/etc, or
46+
# we timed out waiting for still-pending checks.
47+
# We exclude this workflow's own check-run (job name `claude-review`) so the
48+
# gate never waits on or fails because of itself.
49+
- name: Wait for CI; skip the paid review if any check failed
50+
id: gate
51+
env:
52+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
REPO: ${{ github.repository }}
54+
SHA: ${{ github.event.pull_request.head.sha }}
55+
run: |
56+
decision=skip
57+
for i in $(seq 1 72); do # ~24 min max
58+
json=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate \
59+
--jq '[.check_runs[] | select(.name != "claude-review")]' 2>/dev/null) || json=''
60+
[ -z "$json" ] && { sleep 20; continue; }
61+
total=$(jq 'length' <<<"$json")
62+
if [ "$total" -eq 0 ]; then
63+
[ "$i" -ge 9 ] && { decision=run; break; } # ~3 min grace: nothing to gate on
64+
sleep 20; continue
65+
fi
66+
pending=$(jq '[.[]|select(.status!="completed")]|length' <<<"$json")
67+
if [ "$pending" -eq 0 ]; then
68+
bad=$(jq '[.[]|select((.conclusion//"")|test("^(failure|cancelled|timed_out|action_required|stale)$"))]|length' <<<"$json")
69+
[ "$bad" -eq 0 ] && decision=run || decision=skip
70+
break
71+
fi
72+
sleep 20
73+
done
74+
echo "decision=$decision" >> "$GITHUB_OUTPUT"
75+
echo "gate decision: $decision"
76+
3677
- name: Check out PR head (read-only context)
78+
if: steps.gate.outputs.decision == 'run'
3779
# Intentionally tracks the major-version tag (not a pinned SHA) so
3880
# upstream fixes are picked up automatically.
3981
uses: actions/checkout@v4
@@ -44,9 +86,15 @@ jobs:
4486
persist-credentials: false
4587

4688
- name: Run Claude Code Review
89+
if: steps.gate.outputs.decision == 'run'
4790
uses: anthropics/claude-code-action@v1
4891
with:
4992
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
93+
# Provide github_token so the action uses it directly for GitHub API
94+
# calls instead of the OIDC->GitHub-App-token exchange, which 401s under
95+
# pull_request_target. GITHUB_TOKEN is repo/workflow-scoped (independent
96+
# of the actor's role) and has pull-requests: write here.
97+
github_token: ${{ secrets.GITHUB_TOKEN }}
5098
# NOTE: plugin_marketplaces can't be pinned — it tracks the
5199
# marketplace repo's default branch (upstream anthropics/claude-code).
52100
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'

0 commit comments

Comments
 (0)