Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ name: Claude Code Review
# read context (persist-credentials: false) and never build or execute PR code.
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
# labeled: lets adding the claude-debug label (see the "Check for
# claude-debug label" step below) kick off a fresh run by itself, with no
# push/re-run needed. Scoped in the job's `if:` below to only actually
# proceed when the label added IS claude-debug -- otherwise every
# unrelated label added to a PR would trigger another paid review.
types: [opened, synchronize, reopened, ready_for_review, labeled]

concurrency:
group: claude-review-${{ github.event.pull_request.number }}
Comment on lines +18 to 24

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: adding labeled here can silently cancel an in-progress review with nothing replacing it.

The workflow-level concurrency block just below (group: claude-review-${{ github.event.pull_request.number }}, cancel-in-progress: true on the next line) is unchanged by this PR and is keyed only on the PR number. on.pull_request_target.types has no way to filter by which label was added — that filtering only happens in the job's if: (line 42). So labeled fires a new workflow run for any label, not just claude-debug.

Concurrency cancellation is resolved at run-creation time, before job-level if: conditions are evaluated. So the sequence: a review is running (triggered by synchronize) → a maintainer adds an unrelated label (e.g. documentation) → GitHub creates a new run in the same concurrency group → the in-progress review is cancelled → the new run's claude-review job is then skipped by the if: (since the label isn't claude-debug) → no review ever posts for that push, and nothing automatically retriggers it.

Before this PR, labeled wasn't a trigger at all, so this failure mode didn't exist. A fix would need to keep non-claude-debug labeled events out of the shared concurrency group (e.g. fold github.event.label.name into the group key for labeled events), rather than relying solely on the job-level if: to no-op them.

# push/re-run needed. Scoped in the job's `if:` below to only actually
# proceed when the label added IS claude-debug -- otherwise every
# unrelated label added to a PR would trigger another paid review.
types: [opened, synchronize, reopened, ready_for_review, labeled]
concurrency:
group: claude-review-${{ github.event.pull_request.number }}

Expand All @@ -33,7 +38,8 @@ jobs:
# fork-checkout step's safety.
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade'
github.event.pull_request.head.repo.owner.login == 'jnasbyupgrade' &&
(github.event.action != 'labeled' || github.event.label.name == 'claude-debug')
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
Expand All @@ -48,6 +54,28 @@ jobs:
# existing trusted-fork-owner gate below, not instead of it.
actions: write
steps:
# DEBUG MODE: add the "claude-debug" label to a PR to (a) skip the cost
# gate below entirely -- a debug session shouldn't wait 5-20+ min per
# iteration on sibling CI -- and (b) get show_full_output: true on the
# Run Claude Code Review step, dumping the full raw Claude Code JSON
# transcript (including tool results -- see that input's own WARNING
# below) to the job log. This is how you'd catch something like a
# silently-swallowed `--comment` flag (see that step's other comment).
# Queried live via `gh pr view`, not the static event payload, so
# adding the label and clicking "Re-run jobs" on an existing run picks
# it up without needing a new push.
- name: Check for claude-debug label
id: debug
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
enabled=$(gh pr view "$PR" --repo "$REPO" --json labels \
--jq 'any(.labels[]; .name == "claude-debug")')
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
echo "claude-debug label present: $enabled"

# COST GATE: the paid Claude review is the last thing to run. Wait for the
# PR head's OTHER check-runs to finish and only proceed if they are clean.
# If any sibling check failed we skip the review to avoid spending money
Expand All @@ -63,6 +91,7 @@ jobs:
# gate never waits on or fails because of itself.
- name: Wait for CI; skip the paid review if any check failed
id: gate
if: steps.debug.outputs.enabled != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
Expand Down Expand Up @@ -90,7 +119,7 @@ jobs:
echo "gate decision: $decision"

- name: Check out PR head (read-only context)
if: steps.gate.outputs.decision == 'run'
if: steps.debug.outputs.enabled == 'true' || steps.gate.outputs.decision == 'run'
# Intentionally tracks the major-version tag (not a pinned SHA) so
# upstream fixes are picked up automatically.
uses: actions/checkout@v6
Expand All @@ -107,10 +136,15 @@ jobs:
allow-unsafe-pr-checkout: true

- name: Run Claude Code Review
if: steps.gate.outputs.decision == 'run'
if: steps.debug.outputs.enabled == 'true' || steps.gate.outputs.decision == 'run'
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# See the "Check for claude-debug label" step above -- WARNING (from
# this input's own description): outputs ALL Claude messages
# including tool execution results, which may contain secrets, and
# these logs are publicly visible in GitHub Actions.
show_full_output: ${{ steps.debug.outputs.enabled == 'true' }}
# Provide github_token so the action uses it directly for GitHub API
# calls instead of the OIDC->GitHub-App-token exchange, which 401s under
# pull_request_target. GITHUB_TOKEN is repo/workflow-scoped (independent
Expand Down
Loading