#!/usr/bin/env node /** * check-pr-linked-issue.mjs * Checks that a PR body either links an existing issue/PR or inlines an * issue-template-shaped description. Respects conventional commit prefixes — * skips check for docs/chore/build/ci/style/test/revert prefixed PRs. * * Exports: * checkLinkedIssue(prBody, prTitle) → { passed, failures } * hasInlineIssueDescription(prBody) → boolean */ import { fileURLToPath } from 'node:url'; const ISSUE_PATTERNS = [ /(?:fixes|closes|resolves|refs)\s+#\d+/i, /(?:^|[\s(])https:\/\/github\.com\/paperclipai\/paperclip\/issues\/\d+(?=$|[\s),:;!?]|[.](?![\w-]))/i, /(? { const esc = escapeRegExp(label); // Accept markdown headings or bolded/plain labels on their own line. // Examples: "## What happened?", "**Expected behavior**", "Problem:". const pattern = new RegExp( `^\\s*(?:#{1,6}\\s+|\\*\\*\\s*|__\\s*)?${esc}(?:\\s*[:?])?(?:\\s*\\*\\*|\\s*__)?\\s*$`, 'im' ); return pattern.test(body); }); if (hasMatch) matched += 1; } return matched; } export function hasInlineIssueDescription(body) { if (!body || !body.trim()) return false; for (const fieldSet of Object.values(TEMPLATE_FIELDS)) { if (countMatchedFields(body, fieldSet) >= INLINE_DESCRIPTION_MIN_FIELDS) { return true; } } return false; } function parsePrefix(title) { if (!title) return null; const match = title.match(/^([a-z]+)(?:\([^)]*\))?:/); return match ? match[1].toLowerCase() : null; } export function checkLinkedIssue(body, prTitle = '') { const prefix = parsePrefix(prTitle); if (prefix && SKIP_ISSUE_PREFIXES.includes(prefix)) { return { passed: true, failures: [] }; } if (!body || !body.trim()) { return { passed: false, failures: ['PR body is empty — please fill out the PR template'] }; } const linked = ISSUE_PATTERNS.some(p => p.test(body)); const inlined = hasInlineIssueDescription(body); const passed = linked || inlined; return { passed, failures: passed ? [] : [ 'No linked issue or inline issue description found — either tag an existing issue ' + 'with `Fixes #NNN` / `Closes #NNN` / `Refs #NNN`, or describe the underlying issue ' + 'inline in the PR body following one of our issue templates ' + '(https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). ' + 'See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".', ], }; } if (process.argv[1] === fileURLToPath(import.meta.url)) { const body = process.env.PR_BODY ?? ''; const title = process.env.PR_TITLE ?? ''; const result = checkLinkedIssue(body, title); console.log(JSON.stringify(result)); process.exit(result.passed ? 0 : 1); }