Repository navigation
Repo sync #537
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check for Spammy PRs | ||
|
Check warning on line 1 in .github/workflows/check-for-spammy-prs.yml
|
||
| # Flags low-value public PRs and closes PRs that target branches other than main. | ||
| on: | ||
| pull_request_target: | ||
| types: [opened, edited, reopened, synchronize] | ||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: write | ||
| jobs: | ||
| spammy-pr-check: | ||
| name: Label spammy PRs and reject non-main targets | ||
| if: > | ||
| github.repository == 'github/docs' && github.event_name == 'pull_request_target' && | ||
| github.event.pull_request.user.login != 'docs-bot' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 | ||
| with: | ||
| github-token: ${{ secrets.DOCS_BOT_PAT_BASE }} | ||
| script: | | ||
| const owner = 'github' | ||
| const repo = 'docs' | ||
| const pull_number = context.payload.pull_request.number | ||
| const targetsNonMain = context.payload.pull_request.base.ref !== 'main' | ||
| const files = await github.paginate(github.rest.pulls.listFiles, { | ||
| owner, | ||
| repo, | ||
| pull_number, | ||
| per_page: 100, | ||
| }) | ||
| const onlyDeletes = files.length > 0 && files.every(f => f.status === 'removed') | ||
| const onlyDeletesLines = | ||
| files.some(file => file.deletions > 0) && | ||
| files.every(file => file.additions === 0) | ||
| const isEmptyCommit = !files.length | ||
| const touchesTooMany = files.length > 10 | ||
| const isBlankLineEdit = files.length > 0 && files.every(file => { | ||
| const changedLines = (file.patch || '') | ||
| .split('\n') | ||
| .filter(line => /^[+-]/.test(line)) | ||
| return changedLines.length > 0 && | ||
| changedLines.every(line => line.slice(1).trim() === '') | ||
| }) | ||
| const onlyRenames = files.length > 0 && files.every( | ||
| f => f.status === 'renamed' && f.additions === 0 && f.deletions === 0 | ||
| ) | ||
| const onlyIndexMd = files.length === 1 && files[0].filename.split('/').pop() === 'index.md' | ||
| // Content removals are allowed once the Docs team marks the linked issue `help wanted`. | ||
| let hasApprovedIssue = false | ||
| if (onlyDeletesLines || onlyDeletes) { | ||
| try { | ||
| const { repository } = await github.graphql( | ||
| `query($owner: String!, $repo: String!, $number: Int!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| pullRequest(number: $number) { | ||
| closingIssuesReferences(first: 100) { | ||
| nodes { | ||
| repository { | ||
| nameWithOwner | ||
| } | ||
| labels(first: 100) { | ||
| nodes { | ||
| name | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }`, | ||
| { owner, repo, number: pull_number }, | ||
| ) | ||
| const closingIssues = repository?.pullRequest?.closingIssuesReferences?.nodes || [] | ||
| // Issues in other repos don't count, since their owners can add the label themselves. | ||
| hasApprovedIssue = closingIssues.some(issue => | ||
| issue.repository?.nameWithOwner?.toLowerCase() === `${owner}/${repo}` && | ||
| issue.labels.nodes.some(label => label.name.toLowerCase() === 'help wanted'), | ||
| ) | ||
| } catch (error) { | ||
| // Without the lookup, treat the removal as unapproved, as before. | ||
| core.warning(`Could not check linked issues: ${error.message}`) | ||
| } | ||
| } | ||
| const unapprovedLineDeletion = onlyDeletesLines && !hasApprovedIssue | ||
| const unapprovedFileDeletion = onlyDeletes && !hasApprovedIssue | ||
| if ( | ||
| targetsNonMain || | ||
| unapprovedLineDeletion || | ||
| unapprovedFileDeletion || | ||
| isEmptyCommit || | ||
| touchesTooMany || | ||
| isBlankLineEdit || | ||
| onlyRenames || | ||
| onlyIndexMd | ||
| ) { | ||
| await github.rest.issues.addLabels({ | ||
| owner, | ||
| repo, | ||
| issue_number: pull_number, | ||
| labels: ['invalid'], | ||
| }) | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number: pull_number, | ||
| body: targetsNonMain | ||
| ? 'Pull requests must target the `main` branch. This pull request has been closed as invalid. If you think this is incorrect, please mention this in a corresponding issue.' | ||
| : unapprovedLineDeletion | ||
| ? 'This pull request only removes existing content. Before submitting a content-removal pull request, please [open an issue](https://github.com/github/docs/issues/new/choose) explaining the proposed removal and wait for the GitHub Docs team to add the `help wanted` label to it. Once the issue has the `help wanted` label, you can open a new pull request that links the issue with `Closes: #<issue number>` in the description.' | ||
| : "This pull request may have been opened accidentally. I'm going to close it now, but feel free to check out our [contribution guidelines](https://docs.github.com/en/contributing), or raise an issue.", | ||
| }) | ||
| if (targetsNonMain) { | ||
| await github.rest.pulls.update({ | ||
| owner, | ||
| repo, | ||
| pull_number, | ||
| state: 'closed', | ||
| }) | ||
| } | ||
| if (unapprovedLineDeletion) { | ||
| core.setFailed( | ||
| 'This pull request only deletes lines. An approved issue is required first.', | ||
| ) | ||
| } | ||
| } | ||