diff --git a/.github/workflows/claude-review-self.yml b/.github/workflows/claude-review-self.yml index dc302d8..f3f1ca1 100644 --- a/.github/workflows/claude-review-self.yml +++ b/.github/workflows/claude-review-self.yml @@ -37,8 +37,9 @@ name: Claude Review (self) # On a pull request from a fork this degrades rather than misbehaves. The merge # commit is a commit in this repository, so the tooling checkout resolves; the # workflow being run is still the fork's code merged into `master`, but -# `pull_request` gives a fork run no secrets, so `anthropic-api-key` arrives -# empty and the review step fails — it cannot spend tokens. +# `pull_request` gives a fork run no secrets, so both auth secrets arrive +# empty and the job fails its auth-secret check before the review runs — it +# cannot spend tokens. # `require-write-access` is left at its default `true`, which stops a # non-writer's pull request before that. diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index c9c10d8..b45eabd 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -40,6 +40,9 @@ name: Claude Review (shared) # pull-requests: write # secrets: # anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} +# # or, on subscription auth instead of API billing — set exactly one +# # of the two; mapping both fails the job: +# # claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # # The job fails on a blocking finding. Whether that stops a merge is branch # protection's decision, made per repo, and reversible without touching this. @@ -88,8 +91,17 @@ on: default: 'master' secrets: anthropic-api-key: - description: 'Anthropic API key. Mapped by the caller, since the secret name differs per repo.' - required: true + description: >- + Anthropic API key. Set exactly one of this and + claude-code-oauth-token; a step fails the job when both are missing, + and also when both are set. Mapped by the caller, since the secret + name differs per repo. + required: false + claude-code-oauth-token: + description: >- + Claude Code OAuth token (from `claude setup-token`) — the + subscription alternative to an API key. Same exactly-one rule. + required: false # One review per PR. Rapid pushes previously started overlapping reviews that # raced to overwrite the same sticky comment, and paid for every superseded run. @@ -178,6 +190,25 @@ jobs: echo "required status check, which would report an unreviewed PR as reviewed." exit 1 + # Fail before spending a runner minute on a checkout: with neither + # secret the review step errors anyway, only later and less clearly. + # Both is also an error — which credential would win is the CLI's + # undocumented pick, and the two bill different accounts. + - name: Check exactly one auth secret is present + env: + API_KEY: ${{ secrets.anthropic-api-key }} + OAUTH_TOKEN: ${{ secrets.claude-code-oauth-token }} + run: | + if [ -z "$API_KEY" ] && [ -z "$OAUTH_TOKEN" ]; then + echo "::error::pass anthropic-api-key or claude-code-oauth-token; neither is set." + exit 1 + fi + if [ -n "$API_KEY" ] && [ -n "$OAUTH_TOKEN" ]; then + echo "::error::pass only one of anthropic-api-key and claude-code-oauth-token; both are set." + echo "They bill different accounts, so which one wins would be undocumented." + exit 1 + fi + - name: Checkout repository uses: actions/checkout@v4 with: @@ -272,7 +303,10 @@ jobs: id: review uses: anthropics/claude-code-action@v1.0.187 with: + # Both pass through as env vars; the action treats an empty one as + # absent. The guard step above enforces exactly one is set. anthropic_api_key: ${{ secrets.anthropic-api-key }} + claude_code_oauth_token: ${{ secrets.claude-code-oauth-token }} # Passed so the action uses this token directly instead of exchanging # its OIDC token for an Anthropic GitHub App token. `setupGitHubToken` # returns early on it, and the exchange is the *only* thing that runs diff --git a/README.md b/README.md index f48ce26..4bb64e1 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,9 @@ jobs: pull-requests: write secrets: anthropic-api-key: ${{ secrets.CLAUDE_API_KEY }} + # or, on subscription auth instead of API billing — set exactly one of + # the two; mapping both fails the job: + # claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ``` No `id-token: write`: nothing here mints an OIDC token, because the workflow @@ -197,7 +200,11 @@ either, so granting it in a caller has no effect on the token the job runs with. | `tooling-ref` | `master` | Ref this repo's `review/` assets come from; see below | The secret is named, not inherited, because the repos call it different things -(`CLAUDE_API_KEY` vs `CLAUDE_TOKEN`). The `anthropics/claude-code-action` +(`CLAUDE_API_KEY` vs `CLAUDE_TOKEN`). Auth is one of two secrets: an Anthropic +API key, or a Claude Code OAuth token (from `claude setup-token`) for +subscription billing. Set exactly one — a job with neither, or with both, +fails before the checkout; the two bill different accounts, so an undocumented +winner is not a choice the workflow will make silently. The `anthropics/claude-code-action` version is hardcoded rather than an input: `uses:` does not evaluate expressions, and a configurable version is how the consumers ended up on v1.0.182, v1.0.154 and v1.0.134 in the first place. Bump it here and every