Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 6
feat: Add validate-pr composite action#153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # Validate PR | ||
| Validates non-maintainer pull requests against contribution guidelines. | ||
| ## What it does | ||
| 1. **Validates issue references** — Non-maintainer PRs must reference a GitHub issue where the PR author and a maintainer have discussed the approach. PRs that don't meet this requirement are automatically closed with a descriptive comment. | ||
| 2. **Enforces draft status** — All PRs must start as drafts. Non-draft PRs are automatically converted and labeled. | ||
| Maintainers (users with `admin` or `maintain` role) are exempt from all checks. | ||
| ## Usage | ||
| Create `.github/workflows/validate-pr.yml` in your repository: | ||
| ```yaml | ||
| name: Validate PR | ||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened] | ||
| jobs: | ||
| validate-pr: | ||
| runs-on: ubuntu-24.04 | ||
| permissions: | ||
| pull-requests: write | ||
| steps: | ||
| - uses: getsentry/github-workflows/validate-pr@v3 | ||
| with: | ||
| app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }} | ||
| private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }} | ||
| ``` | ||
| ## Inputs | ||
| | Input | Required | Description | | ||
| |-------|----------|-------------| | ||
| | `app-id` | Yes | GitHub App ID for the SDK Maintainer Bot | | ||
| | `private-key` | Yes | GitHub App private key for the SDK Maintainer Bot | | ||
| ## Outputs | ||
| | Output | Description | | ||
| |--------|-------------| | ||
| | `was-closed` | `'true'` if the PR was closed by validation, unset otherwise | | ||
| ## Validation rules | ||
| ### Issue reference check | ||
| The PR body is scanned for issue references in these formats: | ||
| - `#123` (same-repo) | ||
| - `getsentry/repo#123` (cross-repo) | ||
| - `https://github.com/getsentry/repo/issues/123` (full URL) | ||
| - With optional keywords: `Fixes #123`, `Closes getsentry/repo#123`, etc. | ||
| A PR is valid if **any** referenced issue passes all checks: | ||
| - The issue is fetchable and in a `getsentry` repository | ||
| - If the issue has assignees, the PR author must be one of them | ||
| - Both the PR author and a maintainer have participated in the issue discussion | ||
| ### Draft enforcement | ||
| Non-draft PRs are converted to draft and labeled `converted-to-draft` with an informational comment. | ||
| ## Labels | ||
| The action creates these labels automatically (they don't need to exist beforehand): | ||
| - `violating-contribution-guidelines` — added to all closed PRs | ||
| - `missing-issue-reference` — PR body has no issue references | ||
| - `missing-maintainer-discussion` — referenced issue lacks author + maintainer discussion | ||
| - `issue-already-assigned` — referenced issue is assigned to someone else | ||
| - `converted-to-draft` — PR was automatically converted to draft |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| name: 'Validate PR' | ||
| description: 'Validates non-maintainer PRs against contribution guidelines and enforces draft status' | ||
| author: 'Sentry' | ||
| inputs: | ||
| app-id: | ||
| description: 'GitHub App ID for the SDK Maintainer Bot' | ||
| required: true | ||
| private-key: | ||
| description: 'GitHub App private key for the SDK Maintainer Bot' | ||
| required: true | ||
| outputs: | ||
| was-closed: | ||
| description: 'Whether the PR was closed by the validation step' | ||
| value: ${{ steps.validate.outputs.was-closed }} | ||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Generate GitHub App token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 | ||
| with: | ||
| app-id: ${{ inputs.app-id }} | ||
| private-key: ${{ inputs.private-key }} | ||
| - name: Validate PR | ||
| id: validate | ||
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | ||
| with: | ||
| github-token: ${{ steps.app-token.outputs.token }} | ||
| script: | | ||
| const script = require('${{ github.action_path }}/scripts/validate-pr.js'); | ||
| await script({ github, context, core }); | ||
| - name: Convert PR to draft | ||
| if: >- | ||
| steps.validate.outputs.was-closed != 'true' | ||
| && github.event.pull_request.draft == false | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | ||
| PR_URL: ${{ github.event.pull_request.html_url }} | ||
| run: gh pr ready "$PR_URL" --undo | ||
| - name: Label and comment on draft conversion | ||
| if: >- | ||
| steps.validate.outputs.was-closed != 'true' | ||
| && github.event.pull_request.draft == false | ||
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | ||
| with: | ||
| github-token: ${{ steps.app-token.outputs.token }} | ||
| script: | | ||
| const script = require('${{ github.action_path }}/scripts/enforce-draft.js'); | ||
| await script({ github, context, core }); | ||
stephanie-anderson marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // @ts-check | ||
| /** | ||
| * Labels a PR that was converted to draft and leaves an informational comment. | ||
| * Skips if a bot comment already exists (to avoid duplicates on reopen). | ||
| * | ||
| * @param {object} params | ||
| * @param {import('@actions/github').getOctokit} params.github | ||
| * @param {import('@actions/github').context} params.context | ||
| * @param {import('@actions/core')} params.core | ||
| */ | ||
| module.exports = async ({ github, context, core }) => { | ||
| const pullRequest = context.payload.pull_request; | ||
| const repo = context.repo; | ||
| 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, | ||
| }); | ||
stephanie-anderson marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const botComment = comments.data.find(c => | ||
| c.user.type === 'Bot' && | ||
| c.body.includes('automatically converted to draft') | ||
| ); | ||
stephanie-anderson marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (botComment) { | ||
| core.info('Bot comment already exists, skipping.'); | ||
| return; | ||
| } | ||
| const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/${context.payload.repository.default_branch}/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'), | ||
| }); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.