name: Claude Auto Review

on:
  pull_request:
    types: [ready_for_review, opened]
  workflow_call:
    inputs:
      pr_number:
        description: 'PR number to review'
        required: true
        type: number
      extra_prompt:
        description: 'Additional reviewer instructions appended to the standard review prompt'
        required: false
        type: string
        default: ''
      triggered_by:
        description: 'GitHub username that triggered this review (for audit only)'
        required: false
        type: string
        default: ''
    secrets:
      CLAUDE_CODE_OAUTH_TOKEN:
        required: true
      WINDMILL_EE_PRIVATE_ACCESS:
        required: false

concurrency:
  group: claude-review-${{ inputs.pr_number || github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  auto-review:
    runs-on: ubuntu-latest
    # A non-fork PR (head.repo.fork == false) can only be opened by someone with push
    # access to this repo, so fork==false already enforces write access. Do NOT re-add
    # an author_association gate: the pull_request webhook payload reports private org
    # members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently
    # skips auto-review for every private member.
    if: |
      github.event_name == 'workflow_call' ||
      (
        (github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true) &&
        github.event.pull_request.head.repo.fork == false
      )
    permissions:
      contents: read
      pull-requests: read
      id-token: write
    steps:
      # An agent-driven PR flips to ready only after a clean /review round on a
      # draft, marked by an author comment naming the head SHA (pr skill, "Review
      # rounds"). Re-reviewing that same head on ready_for_review is redundant.
      # The marker alone is author attestation, so also require reviewer evidence:
      # a Codex review (posted by github-actions[bot], not forgeable by the author)
      # that predates the marker and carries a non-blocking verdict. Comment-
      # triggered (workflow_call) and opened runs never skip. Keep the three
      # copies of this check in sync (pr-ready-review / codex-pr-review /
      # pi-pr-review); a shared local action would need the repo checked out
      # before the check, which the codex/pi fork paths deliberately avoid.
      - name: Check clean-round marker
        id: marker
        if: github.event_name == 'pull_request' && github.event.action == 'ready_for_review'
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
        run: |
          # Fetch failures fail open (skip=false): an API hiccup must run the
          # review, never skip it or fail the job.
          COMMENTS=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" --paginate | jq -s '[.[][]]') || COMMENTS='[]'
          MARKER_TIME=$(jq -r --arg author "$PR_AUTHOR" --arg marker "✅ Review round clean @ $HEAD_SHA" \
            '[.[] | select(.user.login == $author) | select(.body | contains($marker)) | .created_at] | min // empty' <<<"$COMMENTS")
          CODEX_VERDICT=''
          if [ -n "$MARKER_TIME" ]; then
            # Only Codex evidence that predates the marker counts: the ready-
            # triggered Codex run itself posts after the flip and must not vouch
            # for a sibling reviewer's skip.
            CODEX_VERDICT=$(jq -r --arg mt "$MARKER_TIME" \
              '[.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("## Codex Review")) | select(.created_at < $mt)] | last | .body // ""' <<<"$COMMENTS" \
              | grep -m1 -oE '(Good to merge|Mergeable, but should ideally address nits|Should address issues before merging)' || true)
          fi
          if [ -n "$MARKER_TIME" ] && [ -n "$CODEX_VERDICT" ] && [ "$CODEX_VERDICT" != "Should address issues before merging" ]; then
            echo "Clean review round marker found for $HEAD_SHA with pre-marker non-blocking Codex verdict; skipping redundant review."
            echo "skip=true" >> "$GITHUB_OUTPUT"
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Checkout repository
        if: steps.marker.outputs.skip != 'true'
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Check EE access
        if: steps.marker.outputs.skip != 'true'
        id: ee
        env:
          EE_TOKEN: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
        run: |
          if [ -n "$EE_TOKEN" ]; then
            echo "available=true" >> "$GITHUB_OUTPUT"
            echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_OUTPUT"
          else
            echo "available=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Checkout EE repository
        if: steps.ee.outputs.available == 'true'
        uses: actions/checkout@v5
        with:
          repository: windmill-labs/windmill-ee-private
          path: ./windmill-ee-private
          ref: ${{ steps.ee.outputs.ee_repo_ref }}
          token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
          fetch-depth: 1

      - name: Substitute EE code
        if: steps.ee.outputs.available == 'true'
        run: ./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private

      - name: Resolve PR number
        if: steps.marker.outputs.skip != 'true'
        id: resolve
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          INPUT_PR_NUMBER: ${{ inputs.pr_number }}
          EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
          EVENT_PR_AUTHOR: ${{ github.event.pull_request.user.login }}
        run: |
          if [ -n "$INPUT_PR_NUMBER" ]; then
            PR_NUMBER="$INPUT_PR_NUMBER"
            PR_AUTHOR=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.user.login')
          else
            PR_NUMBER="$EVENT_PR_NUMBER"
            PR_AUTHOR="$EVENT_PR_AUTHOR"
          fi
          echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
          echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT"

      - name: Fetch prior PR discussion
        if: steps.marker.outputs.skip != 'true'
        id: prior
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          PR_NUMBER: ${{ steps.resolve.outputs.pr_number }}
        run: |
          gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \
            --jq '[.[] | {user: .user.login, created_at: .created_at, body: (.body | .[:4000])}] | sort_by(.created_at) | .[-20:]' \
            > prior-comments.json || echo "[]" > prior-comments.json
          jq -r '
            if length == 0 then ""
            else
              "## Prior PR discussion (most recent up to 20 comments)\n\nIf you have already reviewed this PR (look for your own earlier comment), focus on what changed since then per the diff and respect any decisions the human made in replies. Do not re-flag findings the human already pushed back on.\n\n" +
              (map("### @\(.user) (\(.created_at))\n\n\(.body)") | join("\n\n---\n\n"))
            end
          ' prior-comments.json > prior-comments.md

      - name: Read review prompt
        if: steps.marker.outputs.skip != 'true'
        id: review-prompt
        env:
          EXTRA_PROMPT: ${{ inputs.extra_prompt }}
        run: |
          {
            echo 'REVIEW_PROMPT<<EOF'
            cat REVIEW.md
            echo ''
            cat .claude/review-prompt.md
            if [ -n "$EXTRA_PROMPT" ]; then
              echo ''
              echo '## Additional reviewer instructions'
              echo ''
              printf '%s\n' "$EXTRA_PROMPT"
            fi
            if [ -s prior-comments.md ]; then
              echo ''
              cat prior-comments.md
            fi
            echo 'EOF'
          } >> "$GITHUB_ENV"

      - name: Automatic PR Review
        if: steps.marker.outputs.skip != 'true'
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          track_progress: true
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ steps.resolve.outputs.pr_number }}
            PR AUTHOR: ${{ steps.resolve.outputs.pr_author }}

            ${{ env.REVIEW_PROMPT }}
          claude_args: |
            --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
            --model claude-opus-4-8
