| |
| 'use strict'; |
|
|
| |
| module.exports = async ({ github, context, core }) => { |
| const body = context.payload.pull_request.body || ''; |
| const prNum = context.payload.pull_request.number; |
| const MARKER = '<!-- pr-description-check-bot -->'; |
| const owner = context.repo.owner; |
| const repo = context.repo.repo; |
|
|
| |
| function strip(text) { |
| return (text ?? '').replace(/<!--[\s\S]*?-->/g, '').trim(); |
| } |
|
|
| |
| |
| function section(heading) { |
| const m = body.match(new RegExp(`#+\\s+${heading}[\\s\\S]*?(?=\\n#+\\s+|$)`, 'i')); |
| return strip(m?.[0].replace(new RegExp(`#+\\s+${heading}`, 'i'), '') ?? ''); |
| } |
|
|
| const problems = []; |
|
|
| |
| if (section('Summary').length < 20) { |
| problems.push('**Summary** is empty or too short — describe what changed and why.'); |
| } |
|
|
| |
| |
| |
| const linkedSection = section('Linked Issue'); |
| const hasIssueRef = /#\d+\b/.test(linkedSection) || /\/issues\/\d+/.test(linkedSection); |
| if (!linkedSection || !hasIssueRef) { |
| problems.push('**Linked Issue** — add a reference like `Fixes #NNN`, a bare `#NNN`, or a link to the issue.'); |
| } |
|
|
| |
| const typeBlock = body.match(/##\s+Type of Change[\s\S]*?(?=\n##\s|$)/i)?.[0] ?? ''; |
| if (!/- \[x\]/i.test(typeBlock)) { |
| problems.push('**Type of Change** — check at least one box.'); |
| } |
|
|
| |
| if (!/- \[x\] I searched/i.test(body)) { |
| problems.push('**Checklist** — check the duplicate-search box to confirm you searched existing issues and PRs.'); |
| } |
|
|
| |
| |
| |
| const howTo = section('How to Test'); |
| if (howTo.length < 30) { |
| problems.push('**How to Test** — explain how a reviewer can verify this change. Numbered steps, the commands you ran, or a short code block all work — give a sentence or two of real detail (not just "tested locally").'); |
| } |
|
|
| |
| const comments = await github.paginate(github.rest.issues.listComments, { |
| owner, repo, issue_number: prNum, per_page: 100, |
| }); |
| const existing = comments.find(c => (c.body ?? '').includes(MARKER)); |
|
|
| if (problems.length === 0) { |
| if (existing) { |
| await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id }); |
| } |
| } else { |
| const commentBody = [ |
| MARKER, |
| '⚠️ **PR description — action needed**', |
| '', |
| 'The following required sections are missing or incomplete. Please update the PR description to address them:', |
| '', |
| problems.map(p => `- ${p}`).join('\n'), |
| '', |
| '---', |
| '_This comment is deleted automatically once all sections are complete._', |
| ].join('\n'); |
|
|
| if (existing) { |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: commentBody }); |
| } else { |
| await github.rest.issues.createComment({ owner, repo, issue_number: prNum, body: commentBody }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function labelExists(name) { |
| try { |
| await github.rest.issues.getLabel({ owner, repo, name }); |
| return true; |
| } catch (e) { |
| if (e.status === 404) return false; |
| throw e; |
| } |
| } |
|
|
| async function swapLabel(num, add, remove) { |
| if (await labelExists(add)) { |
| await github.rest.issues.addLabels({ owner, repo, issue_number: num, labels: [add] }); |
| } else { |
| core.warning(`Label "${add}" does not exist in the repo — skipping. Create it once to enable labelling.`); |
| } |
| try { |
| await github.rest.issues.removeLabel({ owner, repo, issue_number: num, name: remove }); |
| } catch (e) { |
| if (e.status !== 404 && e.status !== 410) throw e; |
| } |
| } |
|
|
| if (problems.length === 0) { |
| await swapLabel(prNum, 'ready for review', 'needs work'); |
| } else { |
| await swapLabel(prNum, 'needs work', 'ready for review'); |
| core.setFailed(`PR description has ${problems.length} issue(s) — see bot comment for details.`); |
| } |
| }; |
|
|