diff --git a/.github/workflows/claude-assistant.yml b/.github/workflows/claude-assistant.yml index c7729b8..6622021 100644 --- a/.github/workflows/claude-assistant.yml +++ b/.github/workflows/claude-assistant.yml @@ -42,6 +42,39 @@ jobs: with: fetch-depth: 1 + - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured + env: + TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} + run: | + # Deterministic (non-LLM) precondition check — fail fast and + # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is + # missing, rather than letting claude-code-action fail deep in + # its own error output with CLAUDE_OUTCOME: failure and no + # actionable message (see #90 — mac-dev-server-setup#58 wasted a + # CI cycle on exactly this before someone dug through logs to + # find the missing secret). + # + # Deliberately unconditional (no `if:` gate) — unlike + # claude-blocking-review.yml, this workflow has no doc-only/ + # Dependabot skip paths to gate behind. `required: false` on the + # secrets: block above is NOT a signal that the token is + # optional for this job to function: it exists solely to avoid + # a GitHub dispatch-time validation quirk where `required: true` + # combined with the CALLER's job-level `if:` (the @claude-mention + # guard in claude.yml) causes startup_failure on every run, + # including no-op runs where the job would correctly be skipped + # (see 67630a7). By the time this reusable workflow's `run` job + # actually executes, the caller has already confirmed @claude + # was mentioned by an authorized user — there is no code path + # here where the token is genuinely unnecessary. Every fleet + # caller (confirmed via `gh search code`) passes + # secrets.CLAUDE_CODE_OAUTH_TOKEN through unconditionally. + if [ "$TOKEN_SET" != "true" ]; then + echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." + echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." + exit 1 + fi + - name: Run Claude Code id: claude uses: anthropics/claude-code-action@26ec041249acb0a944c0a47b6c0c13f05dbc5b44 # v1 diff --git a/.github/workflows/claude-blocking-review.yml b/.github/workflows/claude-blocking-review.yml index a3f2014..01b72a9 100644 --- a/.github/workflows/claude-blocking-review.yml +++ b/.github/workflows/claude-blocking-review.yml @@ -51,6 +51,14 @@ name: Claude Blocking Review # be scoped to a specific commit; see below. Both changes are additive: # existing unscoped markers and log consumers are unaffected. # +# v3.2.0 (2026-08-07): #90 — a new "Verify CLAUDE_CODE_OAUTH_TOKEN is +# configured" step runs before the Claude review step and fails loudly +# with an actionable ::error:: if the caller's secret resolves empty, +# instead of letting claude-code-action fail deep in its own error output +# (previously misreported as a substantive review BLOCK rather than an +# infrastructure/config gap). Additive: callers with the secret correctly +# configured see no behavior change. +# # Escape hatch: add [skip-claude-review: reason] to the PR body to bypass # enforcement with an audit trail. This unscoped form is honored # unconditionally on every subsequent run for the life of the PR (deliberate @@ -379,6 +387,42 @@ jobs: done <<< "$PRIOR_IDS" echo "Minimized $COUNT prior review comment(s) as OUTDATED." + - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured + if: steps.doc-check.outputs.skip != 'true' && steps.dependabot-check.outputs.skip != 'true' + env: + TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} + run: | + # Deterministic (non-LLM) precondition check — fail fast and + # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is + # missing, rather than letting claude-code-action fail deep in + # its own error output with CLAUDE_OUTCOME: failure and no + # verdict rendered (see #90 — mac-dev-server-setup#58 wasted a + # CI cycle on exactly this: the downstream verdict-parsing logic + # correctly fail-closed to BLOCK, but the real cause — a missing + # secret — was only visible deep in the action's own logs, and + # got misreported as a substantive review BLOCK rather than an + # infrastructure/config gap). + # + # `secrets: claude_oauth_token: required: true` on this + # workflow_call only guarantees the caller supplied a `secrets:` + # block — it does NOT guarantee the underlying repo secret it + # references is actually set. A caller passing + # `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` from a repo where that + # secret was never added still satisfies the `required: true` + # check while passing an empty string through. + # + # Gated behind the same doc-only/Dependabot skip outputs as the + # review step itself (rather than running unconditionally right + # after the permissions preflight) so a repo with a missing + # secret still gets a clean SKIPPED verdict on doc-only or + # Dependabot PRs, instead of failing on PRs that would never + # have invoked claude-code-action anyway. + if [ "$TOKEN_SET" != "true" ]; then + echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." + echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." + exit 1 + fi + - name: Run Claude Code Review id: claude-review if: steps.doc-check.outputs.skip != 'true' && steps.dependabot-check.outputs.skip != 'true'