name: PR Hygiene on: pull_request: types: - opened - edited - reopened - synchronize permissions: contents: read pull-requests: read jobs: validate: runs-on: ubuntu-latest steps: - name: Validate pull request body uses: actions/github-script@v7 with: script: | const body = context.payload.pull_request.body || ""; function getSection(title) { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`##\\s+${escaped}\\s*\\n([\\s\\S]*?)(?=\\n##\\s+|$)`, "i"); const match = body.match(regex); return match ? match[1].trim() : ""; } const linkedIssue = getSection("Linked issue"); const howToTest = getSection("How to test"); const hasIssueReference = /#\d+/.test(linkedIssue) || /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/\d+/i.test(linkedIssue); const sanitizedHowToTest = howToTest .replace(/Describe the verification steps\. If no runtime test applies, explain why\./i, "") .trim(); const failures = []; if (!hasIssueReference) { failures.push("Add an issue reference under `## Linked issue` (for example `Closes #123`)."); } if (!sanitizedHowToTest) { failures.push("Fill in `## How to test` with verification steps or explain why no runtime test applies."); } if (failures.length > 0) { core.setFailed(failures.join("\n")); } else { core.info("Pull request body passed hygiene checks."); }