From cd527ca35469ac2fa770aa40c9f5a0d9b896ba15 Mon Sep 17 00:00:00 2001 From: Stephanie Anderson Date: Fri, 27 Mar 2026 12:06:04 +0100 Subject: [PATCH] ci: Merge PR validation workflows and add reason-specific labels Combine close-unvetted-pr.yml and enforce-draft-pr.yml into a single validate-pr.yml workflow. The vetting job runs first on opened and reopened events; the draft enforcement job depends on it and skips if the PR was already closed. Each closure reason now gets a specific label in addition to violating-contribution-guidelines: missing-issue-reference, missing-maintainer-discussion, or issue-already-assigned. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/enforce-draft-pr.yml | 72 -------------- ...{close-unvetted-pr.yml => validate-pr.yml} | 93 +++++++++++++++++-- 2 files changed, 84 insertions(+), 81 deletions(-) delete mode 100644 .github/workflows/enforce-draft-pr.yml rename .github/workflows/{close-unvetted-pr.yml => validate-pr.yml} (77%) diff --git a/.github/workflows/enforce-draft-pr.yml b/.github/workflows/enforce-draft-pr.yml deleted file mode 100644 index 205a20185c..0000000000 --- a/.github/workflows/enforce-draft-pr.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Enforce Draft PR - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - enforce-draft: - name: Enforce Draft PR - runs-on: ubuntu-24.04 - if: github.event.pull_request.draft == false - permissions: - pull-requests: write - contents: write - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 - with: - app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }} - private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }} - - - name: Convert PR to draft - env: - GH_TOKEN: ${{github.token}} - PR_URL: ${{ github.event.pull_request.html_url }} - run: | - gh pr ready "$PR_URL" --undo - - - name: Label and comment - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const pullRequest = context.payload.pull_request; - const repo = context.repo; - - // Label the PR so maintainers can filter/track violations - await github.rest.issues.addLabels({ - ...repo, - issue_number: pullRequest.number, - labels: ['converted-to-draft'], - }); - - // Check for existing bot comment to avoid duplicates on reopen - const comments = await github.rest.issues.listComments({ - ...repo, - issue_number: pullRequest.number, - }); - const botComment = comments.data.find(c => - c.user.type === 'Bot' && - c.body.includes('automatically converted to draft') - ); - if (botComment) { - core.info('Bot comment already exists, skipping.'); - return; - } - - const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/master/CONTRIBUTING.md`; - - await github.rest.issues.createComment({ - ...repo, - issue_number: pullRequest.number, - body: [ - `This PR has been automatically converted to draft. All PRs must start as drafts per our [contributing guidelines](${contributingUrl}).`, - '', - '**Next steps:**', - '1. Ensure CI passes', - '2. Fill in the PR description completely', - '3. Mark as "Ready for review" when you\'re done' - ].join('\n') - }); diff --git a/.github/workflows/close-unvetted-pr.yml b/.github/workflows/validate-pr.yml similarity index 77% rename from .github/workflows/close-unvetted-pr.yml rename to .github/workflows/validate-pr.yml index c4e4c97a00..5f489cd415 100644 --- a/.github/workflows/close-unvetted-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -1,8 +1,8 @@ -name: Close Unvetted Non-Maintainer PRs +name: Validate PR on: pull_request_target: - types: [opened] + types: [opened, reopened] jobs: validate-non-maintainer-pr: @@ -11,6 +11,8 @@ jobs: permissions: pull-requests: write contents: write + outputs: + was-closed: ${{ steps.validate.outputs.was-closed }} steps: - name: Generate GitHub App token id: app-token @@ -20,6 +22,7 @@ jobs: private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }} - name: Validate PR + id: validate uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.app-token.outputs.token }} @@ -101,12 +104,12 @@ jobs: core.info(`Found ${issueRefs.length} issue reference(s): ${[...seen].join(', ')}`); - // --- Helper: close PR with comment and label --- - async function closePR(message) { + // --- Helper: close PR with comment and labels --- + async function closePR(message, reasonLabel) { await github.rest.issues.addLabels({ ...repo, issue_number: pullRequest.number, - labels: ['violating-contribution-guidelines'], + labels: ['violating-contribution-guidelines', reasonLabel], }); await github.rest.issues.createComment({ @@ -120,6 +123,8 @@ jobs: pull_number: pullRequest.number, state: 'closed', }); + + core.setOutput('was-closed', 'true'); } // --- Step 3: No issue references --- @@ -134,7 +139,7 @@ jobs: '3. Once a maintainer has acknowledged your proposed approach, open a new PR referencing the issue', '', `Please review our [contributing guidelines](${contributingUrl}) for more details.`, - ].join('\n')); + ].join('\n'), 'missing-issue-reference'); return; } @@ -222,7 +227,7 @@ jobs: 'If you believe this assignment is outdated, please comment on the issue to discuss before opening a new PR.', '', `Please review our [contributing guidelines](${contributingUrl}) for more details.`, - ].join('\n')); + ].join('\n'), 'issue-already-assigned'); return; } @@ -234,7 +239,7 @@ jobs: 'To avoid wasted effort on both sides, please discuss your proposed approach in the issue first and wait for a maintainer to respond before opening a PR.', '', `Please review our [contributing guidelines](${contributingUrl}) for more details.`, - ].join('\n')); + ].join('\n'), 'missing-maintainer-discussion'); return; } @@ -249,4 +254,74 @@ jobs: '3. Once a maintainer has acknowledged your proposed approach, open a new PR referencing the issue', '', `Please review our [contributing guidelines](${contributingUrl}) for more details.`, - ].join('\n')); + ].join('\n'), 'missing-issue-reference'); + + enforce-draft: + name: Enforce Draft PR + needs: [validate-non-maintainer-pr] + if: | + always() + && github.event.pull_request.draft == false + && needs.validate-non-maintainer-pr.outputs.was-closed != 'true' + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + contents: write + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 + with: + app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }} + private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }} + + - name: Convert PR to draft + env: + GH_TOKEN: ${{github.token}} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr ready "$PR_URL" --undo + + - name: Label and comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const pullRequest = context.payload.pull_request; + const repo = context.repo; + + // Label the PR so maintainers can filter/track violations + await github.rest.issues.addLabels({ + ...repo, + issue_number: pullRequest.number, + labels: ['converted-to-draft'], + }); + + // Check for existing bot comment to avoid duplicates on reopen + const comments = await github.rest.issues.listComments({ + ...repo, + issue_number: pullRequest.number, + }); + const botComment = comments.data.find(c => + c.user.type === 'Bot' && + c.body.includes('automatically converted to draft') + ); + if (botComment) { + core.info('Bot comment already exists, skipping.'); + return; + } + + const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/master/CONTRIBUTING.md`; + + await github.rest.issues.createComment({ + ...repo, + issue_number: pullRequest.number, + body: [ + `This PR has been automatically converted to draft. All PRs must start as drafts per our [contributing guidelines](${contributingUrl}).`, + '', + '**Next steps:**', + '1. Ensure CI passes', + '2. Fill in the PR description completely', + '3. Mark as "Ready for review" when you\'re done' + ].join('\n') + });