pr-template.yml 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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. dry_run:
  17. description: "Dry run: log what would happen, change nothing"
  18. type: boolean
  19. default: false
  20. permissions:
  21. pull-requests: write
  22. jobs:
  23. template:
  24. runs-on: ubuntu-latest
  25. steps:
  26. - uses: actions/checkout@v4
  27. with:
  28. ref: ${{ github.event.repository.default_branch }}
  29. sparse-checkout: .github/scripts
  30. - uses: actions/github-script@v7
  31. env:
  32. INPUT_PULL_REQUEST: ${{ github.event.inputs.pull_request }}
  33. DRY_RUN: ${{ github.event.inputs.dry_run }}
  34. with:
  35. script: |
  36. const { checkTemplate } = require("./.github/scripts/check-pr-template.js");
  37. const { owner, repo } = context.repo;
  38. const SKIP = ["wsvincent", "jefftriplett", "dependabot[bot]"];
  39. const LABEL = "needs-template";
  40. const MARKER = "<!-- pr-template-check -->";
  41. const dry = process.env.DRY_RUN === "true";
  42. if (dry) core.info("Dry run: nothing will be commented, labeled, closed, or reopened.");
  43. async function check(pr) {
  44. const issue_number = pr.number;
  45. if (SKIP.includes(pr.user.login)) {
  46. core.info(`#${issue_number}: skipped (${pr.user.login})`);
  47. return;
  48. }
  49. const { missing, company, hasPricing } = checkTemplate(pr.body);
  50. const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 });
  51. const existing = comments.find((c) => c.body && c.body.includes(MARKER));
  52. async function say(body) {
  53. body = `${MARKER}\n${body}`;
  54. if (dry) {
  55. core.info(`#${issue_number}: would ${existing ? "update" : "post"} comment:\n${body}`);
  56. } else if (existing) {
  57. await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
  58. } else {
  59. await github.rest.issues.createComment({ owner, repo, issue_number, body });
  60. }
  61. }
  62. if (missing.length) {
  63. core.info(`#${issue_number}: missing ${missing.length} items`);
  64. const list = missing.map((m) => `- ${m}`).join("\n");
  65. await say(
  66. `Thanks for the submission! This pull request does not fill out the ` +
  67. `[pull request template](https://github.com/${owner}/${repo}/blob/main/.github/pull_request_template.md), ` +
  68. `so it was closed automatically.\n\nMissing:\n${list}\n\n` +
  69. `Edit the pull request description to add the missing items. ` +
  70. `The check runs again on every edit and reopens the pull request when the template is complete.`
  71. );
  72. if (dry) {
  73. core.info(`#${issue_number}: would add label "${LABEL}"${pr.state === "open" ? " and close the pull request" : ""}`);
  74. return;
  75. }
  76. await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [LABEL] });
  77. if (pr.state === "open") {
  78. await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "closed" });
  79. }
  80. return;
  81. }
  82. core.info(`#${issue_number}: complete`);
  83. const hadLabel = pr.labels.some((l) => l.name === LABEL);
  84. if (hadLabel && dry) {
  85. core.info(`#${issue_number}: would remove label "${LABEL}"${pr.state === "closed" && !pr.merged_at ? " and reopen the pull request" : ""}`);
  86. } else if (hadLabel) {
  87. await github.rest.issues.removeLabel({ owner, repo, issue_number, name: LABEL }).catch(() => {});
  88. if (pr.state === "closed" && !pr.merged_at) {
  89. await github.rest.pulls.update({ owner, repo, pull_number: issue_number, state: "open" });
  90. }
  91. }
  92. let note = "The pull request template is complete. Thanks! A maintainer will review it.";
  93. if (company) {
  94. note +=
  95. `\n\nThis submission is on behalf of a company. Please read the ` +
  96. `[commercial products and services](https://github.com/${owner}/${repo}/blob/main/contributing.md#commercial-products-and-services) ` +
  97. `section of the contribution guidelines.`;
  98. if (!hasPricing) {
  99. note +=
  100. `\n\nIf this is a paid product, please add a link to your public pricing page under ` +
  101. `question 5 of the template, or a short note on how it is priced.`;
  102. }
  103. }
  104. if (existing || company) await say(note);
  105. }
  106. if (context.payload.pull_request) {
  107. await check(context.payload.pull_request);
  108. } else if (process.env.INPUT_PULL_REQUEST) {
  109. const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(process.env.INPUT_PULL_REQUEST) });
  110. await check(pr);
  111. } else {
  112. const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
  113. for (const pr of prs) await check(pr);
  114. }