## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## Reusable workflow: Anthropic Claude code review on pull requests
## and on @claude mentions in PR/review-comment threads.
##
## Trigger asymmetry vs reusable-codex-review.yml: Claude is
## mention-driven (listens for @claude in issue_comment and
## pull_request_review_comment), so the consumer wrapper carries
## those event triggers. Codex has no mention listener; its
## consumer wrapper relies on pull_request + workflow_dispatch
## (with a pr_ref input for fork PRs). The asymmetry is
## intentional; do not normalize.
##
## ## Trust footprint
##
## anthropics/claude-code-action - SHA-pinned, calls the Anthropic
## API with the caller's ANTHROPIC_API_KEY. Posts review comments
## via the runner's GITHUB_TOKEN scoped to the permissions block on
## the calling job.
##
## ## Action-input drift (2026-05)
##
## The action removed the 'allowed_users' input upstream; the new
## API surface is 'allowed_non_write_users' + 'allowed_bots'. Live
## runs against the SHA pinned below emit:
##
##   ##[warning]Unexpected input(s) 'allowed_users', valid inputs
##     are [..., 'allowed_bots', 'allowed_non_write_users', ...]
##
## We do NOT pass either replacement: the action's default already
## allows any actor with repo write permission, which matches our
## roster (org members all have write). The roster gate is enforced
## one layer up in the job's 'if:' condition - that gate runs
## BEFORE the runner spins up, so the action-internal check would
## be redundant even if it still existed.
##
## ## OIDC federation
##
## The action requests an OIDC token from the runner, exchanges it
## for a GitHub App token via Anthropic's gateway, and uses that
## elevated token to post review comments. Live log:
##
##   Requesting OIDC token...
##   OIDC token successfully obtained
##   Exchanging OIDC token for app token...
##   Using GITHUB_TOKEN from OIDC
##
## 'id-token: write' on the calling job's permissions block is
## therefore required. Without it the action fails with "Could not
## fetch an OIDC token. Did you remember to add 'id-token: write'
## to your workflow permissions?".
##
## ## Secret isolation
##
## 'workflow_call.secrets' is documentation - 'secrets: inherit'
## bypasses it. Callers must use an explicit 'secrets:' map for
## the schema to actually constrain what flows in. Run
## .github/workflows/secrets-audit.yml manually (workflow_dispatch)
## to verify the secrets context.
##
## ## Caller responsibilities
##
## - Define the trigger schedule (typically pull_request +
##   issue_comment + pull_request_review_comment). Schedule cannot
##   live in a reusable workflow per GitHub Actions G-A-002.
## - Hardcode the owner in 'jobs.<id>.uses:' (G-A-001).
## - Forward via explicit 'secrets:' map (see Caller example). Not
##   'inherit'.
## - Set the 'allowed-users' input to the comma-separated list of
##   usernames allowed to trigger @claude. The action errors out
##   on actor checks if this is omitted; an explicit empty string
##   means "everyone allowed" (not recommended on repos with paid
##   API key access).
## - Grant the same job-level permissions block listed in the
##   caller example below. The action federates the runner's
##   id-token (OIDC) to an Anthropic-controlled GitHub App which
##   in turn mints a higher-privilege GITHUB_TOKEN used to post
##   review comments. id-token: write is required.
##
## ## Inputs
##
## - allowed-users    (optional)  Comma-separated GitHub usernames
##                                allowed to trigger @claude. Default
##                                is the org's canonical reviewer
##                                roster (set inline below); pass an
##                                explicit value to override per
##                                repo. An empty string is rejected
##                                by the action.
## - timeout-minutes  (optional)  Job timeout. Default 30.
##
## ## Secrets
##
## - ANTHROPIC_API_KEY  (required)  Anthropic API key. Set at org
##                                  level; every consumer wrapper
##                                  forwards it explicitly via the
##                                  'secrets:' map.
##
## ## Caller example
##
##   name: Claude Code Review
##   on:
##     pull_request:
##     issue_comment:
##       types: [created]
##     pull_request_review_comment:
##       types: [created]
##   permissions:
##     contents: read
##   jobs:
##     review:
##       uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-claude-code-review.yml@master
##       secrets:
##         ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
##       permissions:
##         contents: read
##         pull-requests: write
##         issues: write
##         id-token: write

name: Claude Code Review (reusable)

on:
  workflow_call:
    inputs:
      ## Org-canonical reviewer roster. Single source of truth -
      ## consumer wrappers (kloak/helper-scripts/security-misc/...)
      ## should inherit this default rather than redeclaring the
      ## list per repo. Update here when the roster changes.
      allowed-users:
        required: false
        type: string
        default: "adrelanos,ArrayBolt3,assisted-by-ai"
      timeout-minutes:
        required: false
        type: number
        default: 30
    secrets:
      ANTHROPIC_API_KEY:
        required: true

permissions:
  contents: read

## No reusable-side concurrency block. Per agents/github-actions.md
## G-A-006: github.workflow inside a reusable resolves to the
## caller's workflow name, so a reusable that declares a group key
## the caller cannot disambiguate produces an identical lock to the
## caller's and self-cancels (Actions surfaces this as "Canceling
## since a deadlock was detected for concurrency group: ..."). The
## caller's lock lives in consumer-claude-code.yml and carries the
## PR/issue-number disambiguator required for issue_comment events;
## that lock covers this reusable's runs.

jobs:
  review:
    name: Claude Code Review
    runs-on: ubuntu-latest
    timeout-minutes: ${{ inputs.timeout-minutes }}
    ## Job-level gate. Two purposes:
    ##
    ## 1. On comment-triggered events ('issue_comment',
    ##    'pull_request_review_comment'), only run when the comment
    ##    body actually mentions @claude. Without this check, the
    ##    runner would spin up on every comment, the action would
    ##    no-op internally, and minutes would be burned for nothing.
    ##
    ## 2. Across all event types, restrict to the configured
    ##    'allowed-users' roster. This prevents unauthorized actors
    ##    from even spinning up a runner. Defense in depth: API budget
    ##    protection (random external commenter cannot exhaust the
    ##    org's ANTHROPIC_API_KEY by spam-mentioning @claude) and
    ##    blast-radius reduction (a compromised allowed_users action
    ##    arg cannot bypass this check).
    ##
    ## Substring-match on the comma-separated input is wrapped with
    ## leading/trailing commas in both the haystack and needle so
    ## that "bob" does not accidentally match "alice,bobby,charlie".
    if: |
      contains(format(',{0},', inputs.allowed-users), format(',{0},', github.actor)) && (
        github.event_name == 'pull_request' ||
        (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
        (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
      )
    permissions:
      contents: read
      pull-requests: write
      issues: write
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false

      - name: Run Claude Code Review
        uses: anthropics/claude-code-action@787c5a0ce96a9a6cfb050ea0c8f4c05f2447c251  # v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          ## Note: the action no longer accepts an 'allowed_users'
          ## input (verified against live workflow log: "Unexpected
          ## input(s) 'allowed_users', valid inputs are [...]").
          ## The roster gate is enforced one layer up via the job's
          ## 'if:' condition above.
