pr-template.yml 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. name: Check pull request template
  2. # Runs on pull_request_target so the token can comment, label, and close pull
  3. # requests that come from forks. Only the base branch is checked out, and the
  4. # script reads nothing but the pull request body, so the head commit never runs.
  5. #
  6. # Run it by hand from the Actions tab to check existing pull requests: give a
  7. # number to check one, or leave the field empty to check every open pull request.
  8. on:
  9. pull_request_target:
  10. types: [opened, edited, reopened]
  11. workflow_dispatch:
  12. inputs:
  13. pull_request:
  14. description: "Pull request number (empty = all open pull requests)"
  15. required: false
  16. permissions:
  17. pull-requests: write
  18. jobs:
  19. template:
  20. runs-on: ubuntu-latest
  21. steps:
  22. - uses: actions/checkout@v4
  23. with:
  24. ref: ${{ github.event.repository.default_branch }}
  25. sparse-checkout: .github/scripts
  26. - uses: actions/github-script@v7
  27. env:
  28. INPUT_PULL_REQUEST: ${{ github.event.inputs.pull_request }}
  29. with:
  30. script: |
  31. const { checkTemplate } = require("./.github/scripts/check-pr-template.js");
  32. const { owner, repo } = context.repo;
  33. const SKIP = ["wsvincent", "jefftriplett", "dependabot[bot]"];
  34. const LABEL = "needs-template";
  35. const MARKER = "<!-- pr-template-check -->";
  36. async function check(pr) {
  37. const issue_number = pr.number;
  38. if (SKIP.includes(pr.user.login)) {
  39. core.info(`#${issue_number}: skipped (${pr.user.login})`);
  40. return;
  41. }
  42. const { missing, company } = checkTemplate(pr.body);
  43. const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 });
  44. const existing = comments.find((c) => c.body && c.body.includes(MARKER));
  45. async function say(body) {
  46. body = `${MARKER}\n${body}`;
  47. if (existing) {
  48. await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
  49. } else {
  50. await github.rest.issues.createComment({ owner, repo, issue_number, body });
  51. }
  52. }
  53. if (missing.length) {
  54. core.info(`#${issue_number}: missing ${missing.length} items`);
  55. const list = missing.map((m) => `- ${m}`).join("\n");
  56. await say(
  57. `Thanks for the submission! This pull request does not fill out the ` +
  58. `[pull request template](https://github.com/${owner}/${repo}/blob/main/.github/pull_request_template.md), ` +
  59. `so it was closed automatically.\n\nMissing:\n${list}\n\n` +
  60. `Edit the pull request description to add the missing items. ` +
  61. `The check runs again on every edit and reopens the pull request when the template is complete.`
  62. );
  63. await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [LABEL] });
  64. if (pr.state === "open") {
  65. await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "closed" });
  66. }
  67. return;
  68. }
  69. core.info(`#${issue_number}: complete`);
  70. const hadLabel = pr.labels.some((l) => l.name === LABEL);
  71. if (hadLabel) {
  72. await github.rest.issues.removeLabel({ owner, repo, issue_number, name: LABEL }).catch(() => {});
  73. if (pr.state === "closed" && !pr.merged_at) {
  74. await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "open" });
  75. }
  76. }
  77. let note = "The pull request template is complete. Thanks! A maintainer will review it.";
  78. if (company) {
  79. note +=
  80. `\n\nThis submission is on behalf of a company. Please read the ` +
  81. `[commercial products and services](https://github.com/${owner}/${repo}/blob/main/contributing.md#commercial-products-and-services) ` +
  82. `section of the contribution guidelines.`;
  83. }
  84. if (existing || company) await say(note);
  85. }
  86. if (context.payload.pull_request) {
  87. await check(context.payload.pull_request);
  88. } else if (process.env.INPUT_PULL_REQUEST) {
  89. const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(process.env.INPUT_PULL_REQUEST) });
  90. await check(pr);
  91. } else {
  92. const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
  93. for (const pr of prs) await check(pr);
  94. }