check-pr-template.js 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. // Checks that a pull request body has the answers the template asks for.
  2. // Returns a list of missing items. An empty list means the template is complete.
  3. // Kept separate from the workflow so it can be run locally:
  4. // node .github/scripts/check-pr-template.js < body.md
  5. function section(body, startPattern, endPattern) {
  6. const start = body.search(startPattern);
  7. if (start === -1) return null;
  8. const rest = body.slice(start).replace(startPattern, "");
  9. const end = rest.search(endPattern);
  10. return end === -1 ? rest : rest.slice(0, end);
  11. }
  12. function hasAnswer(text) {
  13. if (text === null) return false;
  14. const cleaned = text
  15. .replace(/_\(.*?\)_/gs, "") // italic placeholder from the template
  16. .replace(/^\s*-\s*\[[ x]\].*$/gim, "") // checkbox lines are not prose answers
  17. .replace(/[-*_\s]/g, "");
  18. return cleaned.length > 0;
  19. }
  20. function hasCheckedBox(text) {
  21. return text !== null && /^\s*-\s*\[x\]/im.test(text);
  22. }
  23. function checkTemplate(body) {
  24. const text = (body || "").replace(/\r\n/g, "\n");
  25. const missing = [];
  26. const questions = [
  27. { n: 1, label: "How long has the project been maintained?" },
  28. { n: 2, label: "How many releases has it had?" },
  29. { n: 4, label: "What makes it awesome?" },
  30. ];
  31. const info = section(text, /## Project Information/i, /^----|^## /m);
  32. if (!hasAnswer(section(info || "", /\*\*Project Name:\*\*/i, /^\s*2\./m))) {
  33. missing.push("Project Information: Project Name");
  34. }
  35. if (!hasAnswer(section(info || "", /\*\*Project URL:\*\*/i, /^\s*3\./m))) {
  36. missing.push("Project Information: Project URL");
  37. }
  38. if (!hasAnswer(section(info || "", /\*\*Description:\*\*/i, /^----|^## /m))) {
  39. missing.push("Project Information: Description");
  40. }
  41. const criteria = section(text, /## Criteria/i, /^## /m) || "";
  42. for (const q of questions) {
  43. const next = new RegExp(`^\\s*${q.n + 1}\\.\\s+\\*\\*|^----|^## `, "m");
  44. const answer = section(criteria, new RegExp(`^\\s*${q.n}\\.\\s+\\*\\*.*?\\*\\*`, "m"), next);
  45. if (!hasAnswer(answer)) missing.push(`Criteria ${q.n}: ${q.label}`);
  46. }
  47. const authorship = section(criteria, /^\s*3\.\s+\*\*.*?\*\*/m, /^\s*4\.\s+\*\*|^----|^## /m);
  48. if (!hasCheckedBox(authorship)) missing.push("Criteria 3: Are you the author? (check one box)");
  49. const disclosure = section(text, /## AI Disclosure/i, /^----|^## /m);
  50. if (!hasCheckedBox(disclosure)) missing.push("AI Disclosure (check one box)");
  51. const company = /^\s*-\s*\[x\].*on behalf of a company/im.test(authorship || "");
  52. return { missing, company };
  53. }
  54. module.exports = { checkTemplate };
  55. if (require.main === module) {
  56. const body = require("fs").readFileSync(0, "utf8");
  57. const result = checkTemplate(body);
  58. console.log(JSON.stringify(result, null, 2));
  59. process.exitCode = result.missing.length ? 1 : 0;
  60. }