name: Check pull request template # Runs on pull_request_target so the token can comment, label, and close pull # requests that come from forks. Only the base branch is checked out, and the # script reads nothing but the pull request body, so the head commit never runs. # # Run it by hand from the Actions tab to check existing pull requests: give a # number to check one, or leave the field empty to check every open pull request. on: pull_request_target: types: [opened, edited, reopened] workflow_dispatch: inputs: pull_request: description: "Pull request number (empty = all open pull requests)" required: false dry_run: description: "Dry run: log what would happen, change nothing" type: boolean default: false permissions: pull-requests: write jobs: template: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.repository.default_branch }} sparse-checkout: .github/scripts - uses: actions/github-script@v7 env: INPUT_PULL_REQUEST: ${{ github.event.inputs.pull_request }} DRY_RUN: ${{ github.event.inputs.dry_run }} with: script: | const { checkTemplate } = require("./.github/scripts/check-pr-template.js"); const { owner, repo } = context.repo; const SKIP = ["wsvincent", "jefftriplett", "dependabot[bot]"]; const LABEL = "needs-template"; const MARKER = ""; const dry = process.env.DRY_RUN === "true"; if (dry) core.info("Dry run: nothing will be commented, labeled, closed, or reopened."); async function check(pr) { const issue_number = pr.number; if (SKIP.includes(pr.user.login)) { core.info(`#${issue_number}: skipped (${pr.user.login})`); return; } const { missing, company, hasPricing } = checkTemplate(pr.body); const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 }); const existing = comments.find((c) => c.body && c.body.includes(MARKER)); async function say(body) { body = `${MARKER}\n${body}`; if (dry) { core.info(`#${issue_number}: would ${existing ? "update" : "post"} comment:\n${body}`); } else if (existing) { await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); } else { await github.rest.issues.createComment({ owner, repo, issue_number, body }); } } if (missing.length) { core.info(`#${issue_number}: missing ${missing.length} items`); const list = missing.map((m) => `- ${m}`).join("\n"); await say( `Thanks for the submission! This pull request does not fill out the ` + `[pull request template](https://github.com/${owner}/${repo}/blob/main/.github/pull_request_template.md), ` + `so it was closed automatically.\n\nMissing:\n${list}\n\n` + `Edit the pull request description to add the missing items. ` + `The check runs again on every edit and reopens the pull request when the template is complete.` ); if (dry) { core.info(`#${issue_number}: would add label "${LABEL}"${pr.state === "open" ? " and close the pull request" : ""}`); return; } await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [LABEL] }); if (pr.state === "open") { await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "closed" }); } return; } core.info(`#${issue_number}: complete`); const hadLabel = pr.labels.some((l) => l.name === LABEL); if (hadLabel && dry) { core.info(`#${issue_number}: would remove label "${LABEL}"${pr.state === "closed" && !pr.merged_at ? " and reopen the pull request" : ""}`); } else if (hadLabel) { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: LABEL }).catch(() => {}); if (pr.state === "closed" && !pr.merged_at) { await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "open" }); } } let note = "The pull request template is complete. Thanks! A maintainer will review it."; if (company) { note += `\n\nThis submission is on behalf of a company. Please read the ` + `[commercial products and services](https://github.com/${owner}/${repo}/blob/main/contributing.md#commercial-products-and-services) ` + `section of the contribution guidelines.`; if (!hasPricing) { note += `\n\nIf this is a paid product, please add a link to your public pricing page under ` + `question 5 of the template, or a short note on how it is priced.`; } } if (existing || company) await say(note); } if (context.payload.pull_request) { await check(context.payload.pull_request); } else if (process.env.INPUT_PULL_REQUEST) { const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(process.env.INPUT_PULL_REQUEST) }); await check(pr); } else { const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 }); for (const pr of prs) await check(pr); }