diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1c1c35f48d..16df71d0b1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,8 +2,13 @@ +HUMAN: + + - [ ] A human has tested these changes. +AGENT: + --- ## Why diff --git a/.github/workflows/pr-readiness-confirm.yml b/.github/workflows/pr-readiness-confirm.yml index 3dfc7f648c..3308071129 100644 --- a/.github/workflows/pr-readiness-confirm.yml +++ b/.github/workflows/pr-readiness-confirm.yml @@ -2,11 +2,11 @@ name: PR Readiness Confirmation on: - # Trigger when a PR is opened (non-draft) or converted from draft to ready. + # Trigger when a PR is opened, edited while ready for review, or converted from draft. pull_request: - types: [opened, ready_for_review] + types: [opened, edited, ready_for_review] pull_request_target: - types: [opened, ready_for_review] + types: [opened, edited, ready_for_review] # Daily check for stale unconfirmed PRs. schedule: @@ -14,13 +14,147 @@ on: 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 = ''; + 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(//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 && ( ( diff --git a/AGENTS.md b/AGENTS.md index 6d47a8c23e..c7f3d4dab0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -285,6 +285,9 @@ Each integration follows a consistent pattern with service classes, storage mode ## Template for Github Pull Request If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`. +- The PR template now starts with a `HUMAN:` section, the human-tested checkbox, and an `AGENT:` section. +- `.github/workflows/pr-readiness-confirm.yml` checks non-draft PRs for non-empty text between `HUMAN:` and the human-tested checkbox; if present it adds a 👍 reaction, and if absent it posts a reminder comment. + ## Implementation Details