---
name: PR Readiness Confirmation

on:
  # Trigger when a PR is opened, edited while ready for review, or converted from draft.
  pull_request:
    types: [opened, edited, ready_for_review]
  pull_request_target:
    types: [opened, edited, ready_for_review]

  # Daily check for stale unconfirmed PRs.
  schedule:
    - cron: '0 9 * * *'

permissions:
  contents: read
  issues: write
  pull-requests: write

jobs:
  # ── Require a short human-written summary on ready-for-review PRs ──
  check-human-context:
    if: |
      github.event_name != 'schedule' &&
      github.event.pull_request.draft == false &&
      (
        (
          github.event_name == 'pull_request' &&
          github.event.pull_request.head.repo.full_name == github.repository
        ) ||
        (
          github.event_name == 'pull_request_target' &&
          github.event.pull_request.head.repo.full_name != github.repository
        )
      )
    concurrency:
      group: pr-human-context-${{ github.event.pull_request.number }}
      cancel-in-progress: true
    runs-on: ubuntu-24.04
    steps:
      - name: Validate HUMAN section
        uses: actions/github-script@v9
        with:
          script: |
            const reminderMarker = '<!-- pr-human-context-required -->';
            const humanTestedCheckboxText = 'A human has tested these changes.';
            const pr = context.payload.pull_request;
            const issueNumber = pr.number;
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const body = pr.body || '';

            const escapeRegex = (text) =>
              text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const humanSectionPattern = new RegExp(
              `(?:^|\\n)HUMAN:\\s*\\n([\\s\\S]*?)(?=\\n(?:- )?\\[[ xX]\\] ${escapeRegex(humanTestedCheckboxText)})`
            );
            const warnOnly = async (label, operation) => {
              try {
                return await operation();
              } catch (error) {
                core.warning(`${label}: ${error.message}`);
                return null;
              }
            };

            const extractHumanSection = (text) => {
              const match = text.match(humanSectionPattern);
              if (!match) {
                return '';
              }
              return match[1].replace(/<!--[\s\S]*?-->/g, '').trim();
            };

            const humanSection = extractHumanSection(body);
            const hasHumanText = humanSection
              .split(/\r?\n/)
              .some((line) => line.trim().length > 0);

            const comments = await warnOnly('Failed to list PR comments', () =>
              github.paginate(github.rest.issues.listComments, {
                owner,
                repo,
                issue_number: issueNumber,
              })
            );
            if (!comments) {
              return;
            }
            const existingReminder = comments.find((comment) =>
              comment.body.includes(reminderMarker)
            );

            if (hasHumanText) {
              if (existingReminder) {
                await warnOnly('Failed to delete HUMAN reminder comment', () =>
                  github.rest.issues.deleteComment({
                    owner,
                    repo,
                    comment_id: existingReminder.id,
                  })
                );
              }

              const reactions = await warnOnly('Failed to list PR reactions', () =>
                github.paginate(github.rest.reactions.listForIssue, {
                  owner,
                  repo,
                  issue_number: issueNumber,
                })
              );
              if (!reactions) {
                return;
              }
              const botAlreadyReacted = reactions.some(
                (reaction) =>
                  reaction.user?.login === 'github-actions[bot]' &&
                  reaction.content === '+1'
              );

              if (!botAlreadyReacted) {
                await warnOnly('Failed to add HUMAN thumbs up reaction', () =>
                  github.rest.reactions.createForIssue({
                    owner,
                    repo,
                    issue_number: issueNumber,
                    content: '+1',
                  })
                );
              }

              return;
            }

            if (!existingReminder) {
              await warnOnly('Failed to create HUMAN reminder comment', () =>
                github.rest.issues.createComment({
                  owner,
                  repo,
                  issue_number: issueNumber,
                  body: [
                    reminderMarker,
                    '👋 Thanks for opening this PR!',
                    '',
                    'Before review, please add a short note in the `HUMAN:` section at the top of the PR description telling maintainers, in your own words, what this PR does.',
                    '',
                    `Please put that note between \`HUMAN:\` and the \`- [ ] ${humanTestedCheckboxText}\` checkbox.`,
                  ].join('\n'),
                })
              );
            }

  # ── Post a confirmation request when a PR becomes ready for review ──
  ask-confirmation:
    if: |
      github.event_name != 'schedule' &&
      github.event.action != 'edited' &&
      github.event.pull_request.draft == false &&
      (
        (
          github.event_name == 'pull_request' &&
          github.event.pull_request.head.repo.full_name == github.repository
        ) ||
        (
          github.event_name == 'pull_request_target' &&
          github.event.pull_request.head.repo.full_name != github.repository
        )
      ) &&
      (
        github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' ||
        github.event.pull_request.author_association == 'NONE'
      )
    concurrency:
      group: pr-readiness-confirm-${{ github.event.pull_request.number }}
      cancel-in-progress: true
    runs-on: ubuntu-24.04
    steps:
      - name: Check for existing confirmation comment
        id: check
        uses: actions/github-script@v9
        with:
          script: |
            const marker = '<!-- pr-readiness-confirm -->';
            const comments = await github.paginate(github.rest.issues.listComments, {
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
            });
            const existing = comments.find(c => c.body.includes(marker));
            core.setOutput('exists', existing ? 'true' : 'false');

      - name: Post confirmation request
        if: steps.check.outputs.exists != 'true'
        uses: actions/github-script@v9
        with:
          script: |
            const marker = '<!-- pr-readiness-confirm -->';
            const body = [
              marker,
              '👋 Thanks for opening this PR!',
              '',
              'Your PR is marked for review. Can you confirm with a 👍 reaction **on this comment** or a reply that this PR is ready to review?',
              '',
              '> If no confirmation is received within 7 days, this PR will be converted back to a draft.',
            ].join('\n');

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body,
            });

  # ── Convert stale unconfirmed PRs back to draft ──
  check-stale-unconfirmed:
    if: github.event_name == 'schedule' && github.repository == 'OpenHands/OpenHands'
    runs-on: ubuntu-24.04
    steps:
      - name: Find and convert stale unconfirmed PRs
        uses: actions/github-script@v9
        with:
          script: |
            const STALE_DAYS = 7;
            const marker = '<!-- pr-readiness-confirm -->';
            const cutoff = new Date(Date.now() - STALE_DAYS * 24 * 60 * 60 * 1000);

            // List open, non-draft PRs
            const prs = await github.paginate(github.rest.pulls.list, {
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: 'open',
              per_page: 100,
            });

            for (const pr of prs) {
              if (pr.draft) continue;

              // Look for the bot's confirmation comment
              const comments = await github.paginate(github.rest.issues.listComments, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
              });

              const botComment = comments.find(c => c.body.includes(marker));
              if (!botComment) continue;

              const commentDate = new Date(botComment.created_at);
              if (commentDate > cutoff) continue; // not stale yet

              // Check for a 👍 reaction from the PR author
              const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
              });
              const authorReacted = reactions.some(
                r => r.user.login === pr.user.login && r.content === '+1'
              );
              if (authorReacted) continue;

              // Check for any comment from the PR author after the bot comment
              const authorReplied = comments.some(
                c => c.user.login === pr.user.login &&
                     new Date(c.created_at) > commentDate
              );
              if (authorReplied) continue;

              // Convert to draft via GraphQL
              try {
                core.info(`Converting PR #${pr.number} back to draft (no confirmation after ${STALE_DAYS} days)`);
                await github.graphql(`
                  mutation($id: ID!) {
                    convertPullRequestToDraft(input: { pullRequestId: $id }) {
                      pullRequest { number }
                    }
                  }
                `, { id: pr.node_id });

                await github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  body: [
                    `This PR has been converted back to a draft because no confirmation was received within ${STALE_DAYS} days.`,
                    '',
                    'When you\'re ready, please mark it as ready for review again.',
                  ].join('\n'),
                });
              } catch (error) {
                core.warning(`Failed to convert PR #${pr.number} to draft: ${error.message}`);
              }
            }
