From 68401f82f3ad12bbc0017051cbf2a842f2eac1f5 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 2 Jun 2026 17:12:41 -0700 Subject: [PATCH] Remove linked-issue gate from commitperclip (#7423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The `commitperclip` PR quality gates enforce hygiene on every PR before merge > - One of those gates required PRs to link to a tracking issue, which adds friction for small/internal changes that don't need a tracker entry > - The repository owner decided the linked-issue requirement is no longer the right default > - This pull request removes the linked-issue gate (the script, its tests, and the orchestrator wiring) > - The benefit is fewer false-failing PR checks and one less mandatory authoring step ## What Changed - Deleted `.github/scripts/check-pr-linked-issue.mjs` - Deleted `.github/scripts/tests/check-pr-linked-issue.test.mjs` - Removed the `checkLinkedIssue` import, the `Promise.resolve(checkLinkedIssue(...))` entry in the `Promise.all` block, the `issueResult` destructured binding, and the `...issueResult.failures` spread from `.github/scripts/run-quality-gates.mjs` ## Verification - `node --test .github/scripts/tests/*.test.mjs` — 72/72 tests pass across the remaining 4 gate suites - `git grep -n 'check-pr-linked-issue\|checkLinkedIssue\|check-pr-linked'` — no matches - Inspected `run-quality-gates.mjs` — no orphaned `issueResult` references ## Risks - Low risk. Pure removal of one optional gate; the `.github/workflows/commitperclip-review.yml` workflow only invokes the orchestrator and needs no changes. PR template and `CONTRIBUTING.md` do not mention linked issues, so no docs change is required. ## Model Used - Claude (Anthropic), `claude-opus-4-7`, extended-thinking mode, tool use enabled ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable (existing gate-suite tests still pass; removed gate's tests deleted with it) - [x] If this change affects the UI, I have included before/after screenshots (n/a — CI script change) - [x] I have updated relevant documentation to reflect my changes (no docs reference the removed gate) - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- .github/scripts/check-pr-linked-issue.mjs | 53 --------- .github/scripts/run-quality-gates.mjs | 5 +- .../tests/check-pr-linked-issue.test.mjs | 111 ------------------ 3 files changed, 1 insertion(+), 168 deletions(-) delete mode 100644 .github/scripts/check-pr-linked-issue.mjs delete mode 100644 .github/scripts/tests/check-pr-linked-issue.test.mjs diff --git a/.github/scripts/check-pr-linked-issue.mjs b/.github/scripts/check-pr-linked-issue.mjs deleted file mode 100644 index 865b8865..00000000 --- a/.github/scripts/check-pr-linked-issue.mjs +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env node -/** - * check-pr-linked-issue.mjs - * Checks that a PR body references a GitHub issue. Respects conventional commit - * prefixes — skips check for docs/chore/build/ci/style/test prefixed PRs. - * Export: checkLinkedIssue(prBody: string, prTitle: string) → { passed, failures } - */ -import { fileURLToPath } from 'node:url'; - -const ISSUE_PATTERNS = [ - /(?:fixes|closes|resolves)\s+#\d+/i, - /(?:^|[\s(])https:\/\/github\.com\/paperclipai\/paperclip\/issues\/\d+(?=$|[\s),:;!?]|[.](?![\w-]))/i, - /(? p.test(body)); - return { - passed: found, - failures: found ? [] : [ - 'No linked issue found — please add `Fixes #NNN` to your PR description. ' + - 'If no issue exists yet, please file one first: ' + - 'https://github.com/paperclipai/paperclip/issues/new', - ], - }; -} - -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); -} diff --git a/.github/scripts/run-quality-gates.mjs b/.github/scripts/run-quality-gates.mjs index a5b6abae..8cf1ad09 100644 --- a/.github/scripts/run-quality-gates.mjs +++ b/.github/scripts/run-quality-gates.mjs @@ -11,7 +11,6 @@ import { fileURLToPath } from 'node:url'; import { ghFetch } from './get-bot-token.mjs'; import { fetchAllPullRequestFiles } from './fetch-pr-files.mjs'; import { checkTemplate } from './check-pr-template.mjs'; -import { checkLinkedIssue } from './check-pr-linked-issue.mjs'; import { checkTestCoverage } from './check-pr-test-coverage.mjs'; import { checkLockfile } from './check-pr-lockfile.mjs'; import { checkDependencies } from './check-pr-dependencies.mjs'; @@ -110,10 +109,9 @@ async function main() { // Run all quality gates (pure functions run sync, deps check is async) const prTitle = pr.title ?? ''; - const [templateResult, issueResult, testResult, lockfileResult, depsResult] = + const [templateResult, testResult, lockfileResult, depsResult] = await Promise.all([ Promise.resolve(checkTemplate(prBody)), - Promise.resolve(checkLinkedIssue(prBody, prTitle)), Promise.resolve(checkTestCoverage(files, prTitle)), Promise.resolve(checkLockfile(files, author, branch)), checkDependencies(files, GH_TOKEN, GH_REPO, prNumber, pr.base?.ref), @@ -121,7 +119,6 @@ async function main() { const allFailures = [ ...templateResult.failures, - ...issueResult.failures, ...testResult.failures, ...lockfileResult.failures, ]; diff --git a/.github/scripts/tests/check-pr-linked-issue.test.mjs b/.github/scripts/tests/check-pr-linked-issue.test.mjs deleted file mode 100644 index 6b2745db..00000000 --- a/.github/scripts/tests/check-pr-linked-issue.test.mjs +++ /dev/null @@ -1,111 +0,0 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { checkLinkedIssue } from '../check-pr-linked-issue.mjs'; - -// Existing tests with title parameter added (defaults to no prefix, so still required) - -test('passes with bare #NNN reference', () => { - assert.equal(checkLinkedIssue('This fixes the bug in #123', 'fix: something').passed, true); -}); - -test('passes with "Fixes #NNN"', () => { - assert.equal(checkLinkedIssue('Fixes #456\n\nSome description', 'fix: something').passed, true); -}); - -test('passes with "Closes #NNN" (case-insensitive)', () => { - assert.equal(checkLinkedIssue('closes #789', 'fix: something').passed, true); -}); - -test('passes with "Resolves #NNN"', () => { - assert.equal(checkLinkedIssue('Resolves #101', 'fix: something').passed, true); -}); - -test('passes with full github.com URL', () => { - assert.equal( - checkLinkedIssue('See https://github.com/paperclipai/paperclip/issues/202', 'fix: bug').passed, - true - ); -}); - -test('passes with a full github.com URL followed by punctuation', () => { - assert.equal( - checkLinkedIssue('See (https://github.com/paperclipai/paperclip/issues/202).', 'fix: bug').passed, - true - ); -}); - -test('fails with empty body when no skip prefix', () => { - const result = checkLinkedIssue('', 'fix: bug'); - assert.equal(result.passed, false); - assert.ok(result.failures.length > 0); -}); - -test('fails with no issue reference when no skip prefix', () => { - const result = checkLinkedIssue('Added a cool feature, no issue linked', 'feat: something'); - assert.equal(result.passed, false); - assert.ok(result.failures[0].includes('Fixes #NNN')); -}); - -test('fails with cross-repo issue reference', () => { - const result = checkLinkedIssue('See https://github.com/other/repo/issues/123', 'fix: bug'); - assert.equal(result.passed, false); -}); - -test('fails when the Paperclip issue URL is embedded inside another host', () => { - const result = checkLinkedIssue( - 'See https://evil.example/https://github.com/paperclipai/paperclip/issues/123', - 'fix: bug' - ); - assert.equal(result.passed, false); -}); - -test('fails when the Paperclip issue URL continues into another host', () => { - const result = checkLinkedIssue( - 'See https://github.com/paperclipai/paperclip/issues/123.evil.example', - 'fix: bug' - ); - assert.equal(result.passed, false); -}); - -test('fails when #NNN is part of a word (no space before)', () => { - const result = checkLinkedIssue('This is version#123 not an issue link', 'fix: bug'); - assert.equal(result.passed, false); -}); - -// New tests for prefix-aware skip behavior - -test('skips check for docs: prefix', () => { - assert.equal(checkLinkedIssue('', 'docs: update README').passed, true); -}); - -test('skips check for chore: prefix', () => { - assert.equal(checkLinkedIssue('', 'chore: bump deps').passed, true); -}); - -test('skips check for build: prefix', () => { - assert.equal(checkLinkedIssue('', 'build: update Dockerfile').passed, true); -}); - -test('skips check for ci: prefix', () => { - assert.equal(checkLinkedIssue('', 'ci: add workflow').passed, true); -}); - -test('skips check for test: prefix', () => { - assert.equal(checkLinkedIssue('', 'test: add coverage').passed, true); -}); - -test('skips check with scoped prefix like docs(api):', () => { - assert.equal(checkLinkedIssue('', 'docs(api): document endpoint').passed, true); -}); - -test('requires issue for feat: prefix', () => { - assert.equal(checkLinkedIssue('Some description without issue', 'feat: new thing').passed, false); -}); - -test('requires issue for refactor: prefix', () => { - assert.equal(checkLinkedIssue('Some refactor', 'refactor: rewrite thing').passed, false); -}); - -test('requires issue when no prefix (encourages prefix usage)', () => { - assert.equal(checkLinkedIssue('No prefix here', 'Add some feature').passed, false); -});