# VERY IMPORTANT:
# This workflow has a lot of permissions!
# It should ONLY run on trusted maintainers code (e.g. wwwillchen)
name: PR Review Responder

on:
  workflow_run:
    workflows: ["CI"]
    # Regardless of success or fail, we want to run this workflow.
    # Why?
    # If it's failure, we want to fix the errors.
    # If it's success, we want to address the PR review comments from the AI code reviewers.
    #
    # The CI workflow is almost always the last workflow to finish, so that's why we wait for it.
    types: [completed]

jobs:
  respond-to-pr:
    environment: ai-bots
    runs-on: ubuntu-latest
    permissions:
      actions: write
      contents: write
      pull-requests: write
    steps:
      - name: Get PR info and check labels
        id: pr-info
        uses: actions/github-script@v7
        with:
          script: |
            const run = context.payload.workflow_run;

            // Get PR associated with this workflow run
            // Note: run.pull_requests can be empty for cross-repo PRs (forks)
            // So we also search by head SHA as a fallback
            let prNumber;

            if (run.pull_requests && run.pull_requests.length > 0) {
              prNumber = run.pull_requests[0].number;
            } else {
              // Search for PRs with matching head branch
              // Note: head_repository can be null if the fork was deleted
              if (!run.head_repository) {
                console.log('Head repository not available');
                core.setOutput('should_continue', 'false');
                return;
              }
              const { data: prs } = await github.rest.pulls.list({
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: 'open',
                head: `${run.head_repository.owner.login}:${run.head_branch}`
              });

              if (prs.length === 0) {
                console.log('No pull requests found for this workflow run');
                core.setOutput('should_continue', 'false');
                return;
              }
              prNumber = prs[0].number;
            }

            // Fetch full PR details to get labels
            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: prNumber
            });

            // Only allow wwwillchen and wwwillchen-bot to use this workflow
            if (pr.user.login !== 'wwwillchen' && pr.user.login !== 'wwwillchen-bot') {
              console.log(`PR #${prNumber} author ${pr.user.login} is not allowed to use this workflow`);
              core.setOutput('should_continue', 'false');
              return;
            }

            // Check for cc:request (initial) or cc:request:N (re-request) labels
            // If multiple labels exist, use the highest count to avoid resetting the retry counter
            let requestCount = -1;
            let currentLabel = '';

            for (const label of pr.labels) {
              if (label.name === 'cc:request') {
                if (requestCount < 0) {
                  requestCount = 0;
                  currentLabel = 'cc:request';
                }
              }
              const match = label.name.match(/^cc:request:(\d+)$/);
              if (match) {
                const count = parseInt(match[1], 10);
                if (count > requestCount) {
                  requestCount = count;
                  currentLabel = label.name;
                }
              }
            }

            if (requestCount === -1) {
              console.log(`PR #${prNumber} does not have a cc:request or cc:request:N label`);
              core.setOutput('should_continue', 'false');
              return;
            }

            // Guard: do not loop more than 3 retries (4 total runs: initial + 3 retries)
            // cc:request (0) -> cc:request:1 (1) -> cc:request:2 (2) -> cc:request:3 (3) -> blocked
            if (requestCount >= 4) {
              console.log(`PR #${prNumber} has reached max retry count (${requestCount}), adding needs-human:review-issue`);

              // Remove current label, cc:pending
              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                name: currentLabel
              }).catch(() => {});

              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                name: 'cc:pending'
              }).catch(() => {});

              // Also add needs-human:review-issue (retries exhausted = needs human attention).
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                labels: ['needs-human:review-issue']
              });

              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                name: 'needs-human:final-check'
              }).catch(() => {});

              core.setOutput('pr_number', prNumber);
              core.setOutput('should_continue', 'false');
              return;
            }

            console.log(`PR #${prNumber} has ${currentLabel} label (count=${requestCount}), proceeding with pr-fix`);
            core.setOutput('pr_number', prNumber);
            core.setOutput('should_continue', 'true');
            core.setOutput('request_count', requestCount);
            core.setOutput('current_label', currentLabel);
            core.setOutput('head_repo', run.head_repository.full_name);

      - name: Checkout repository
        if: steps.pr-info.outputs.should_continue == 'true'
        uses: actions/checkout@v5
        with:
          # Security note: The pr.user.login check verifies who opened the PR, but this
          # checks out code from whoever last pushed. If "Allow edits from maintainers" is
          # enabled, other maintainers could push to the branch. This is acceptable because
          # maintainers already have write access to the repo and are trusted.
          repository: ${{ github.event.workflow_run.head_repository.full_name }}
          ref: ${{ github.event.workflow_run.head_branch }}
          fetch-depth: 0

      - name: Record HEAD before Claude Code
        if: steps.pr-info.outputs.should_continue == 'true'
        id: before-claude
        run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT

      - name: Configure push remote for fork PRs
        if: steps.pr-info.outputs.should_continue == 'true'
        run: |
          # claude-code-action overwrites origin's fetch URL to point to dyad-sh/dyad
          # (using GITHUB_REPOSITORY which is always the base repo in workflow_run events).
          # Setting pushurl separately ensures git push still targets the fork,
          # because git uses pushurl over url when both are configured.
          #
          # We use PR_CONTENTS_RW_GITHUB_TOKEN (a PAT) instead of GITHUB_TOKEN so that
          # the push creates real PR events (synchronize) that trigger downstream workflows
          # like CI. Pushes made with GITHUB_TOKEN are silently ignored by GitHub to prevent
          # infinite loops.
          git remote set-url --push origin "https://x-access-token:${PR_CONTENTS_RW_GITHUB_TOKEN}@github.com/${{ steps.pr-info.outputs.head_repo }}.git"
          echo "Configured pushurl to ${{ steps.pr-info.outputs.head_repo }}"
        env:
          PR_CONTENTS_RW_GITHUB_TOKEN: ${{ secrets.PR_CONTENTS_RW_GITHUB_TOKEN }}

      - name: Update labels to pending
        if: steps.pr-info.outputs.should_continue == 'true'
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "${{ steps.pr-info.outputs.current_label }}" --add-label "cc:pending"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Run PR Fix
        if: steps.pr-info.outputs.should_continue == 'true'
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          claude_args: --model claude-opus-4-6
          prompt: |
            Run the following skill end-to-end. Execute every step sequentially. Do not skip, summarize, or stop early.

            /dyad:pr-fix ${{ steps.pr-info.outputs.pr_number }}

      - name: Check if commits were pushed
        # Use always() to ensure we detect commits even if Claude Code fails partway through.
        # The push itself (made with PR_CONTENTS_RW_GITHUB_TOKEN) triggers downstream workflows
        # like CI, BugBot, and Claude PR Review naturally via pull_request synchronize events.
        if: steps.pr-info.outputs.should_continue == 'true' && always()
        id: retrigger
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Use the GitHub API to get the current head SHA, avoiding race conditions with git fetch
          PR_HEAD_SHA=$(gh pr view ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --json headRefOid --jq '.headRefOid')
          if [ -z "$PR_HEAD_SHA" ]; then
            echo "Failed to get PR head SHA, falling back to git fetch"
            git fetch origin ${{ github.event.workflow_run.head_branch }} || git fetch origin
            PR_HEAD_SHA=$(git rev-parse FETCH_HEAD 2>/dev/null || git rev-parse origin/${{ github.event.workflow_run.head_branch }})
          fi

          if [ "${{ steps.before-claude.outputs.sha }}" != "$PR_HEAD_SHA" ]; then
            echo "Claude pushed new commits (before: ${{ steps.before-claude.outputs.sha }}, after: $PR_HEAD_SHA)"
            echo "commits_pushed=true" >> $GITHUB_OUTPUT
          else
            echo "No new commits pushed"
            echo "commits_pushed=false" >> $GITHUB_OUTPUT
          fi

      - name: Update labels - re-request review (commits pushed)
        # Use always() so retry loop continues even if Claude Code fails after pushing commits
        if: steps.pr-info.outputs.should_continue == 'true' && always() && steps.retrigger.outputs.commits_pushed == 'true'
        run: |
          REQUEST_COUNT="${{ steps.pr-info.outputs.request_count }}"
          NEXT_COUNT=$(( ${REQUEST_COUNT:-0} + 1 ))
          echo "Setting cc:request:${NEXT_COUNT} to auto-re-request review (retry ${NEXT_COUNT} of 3)"
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:request:${NEXT_COUNT}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Update labels to done (no new commits)
        if: steps.pr-info.outputs.should_continue == 'true' && success() && steps.retrigger.outputs.commits_pushed == 'false'
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:done"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Update labels to failed
        if: steps.pr-info.outputs.should_continue == 'true' && failure()
        run: |
          gh pr edit ${{ steps.pr-info.outputs.pr_number }} --repo ${{ github.repository }} --remove-label "cc:pending" --add-label "cc:failed"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Checkout base repo for labeler script
        # The earlier checkout (line 186) checks out the PR head repo, which for
        # fork PRs may not contain scripts/pr-status-labeler.js. Checkout the
        # base repo's default branch to a separate path so the script is always available.
        if: >-
          steps.pr-info.outputs.should_continue == 'true' &&
          always() &&
          steps.retrigger.outputs.commits_pushed != 'true'
        uses: actions/checkout@v5
        with:
          path: __base
          sparse-checkout: scripts/pr-status-labeler.js

      - name: Apply needs-human status label
        # Run when PR reaches a terminal state (cc:done or cc:failed) but NOT when
        # commits were pushed (retry loop continues). Uses always() to run even if
        # the Claude Code step failed.
        if: >-
          steps.pr-info.outputs.should_continue == 'true' &&
          always() &&
          steps.retrigger.outputs.commits_pushed != 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const { run } = require('./__base/scripts/pr-status-labeler.js');
            await run({
              github,
              context,
              core,
              prNumber: parseInt('${{ steps.pr-info.outputs.pr_number }}', 10),
              ciConclusion: '${{ github.event.workflow_run.conclusion }}'
            });
