From 3ea2461c8ad9ea5e48b1fc84ab46d20494cb0a70 Mon Sep 17 00:00:00 2001 From: Tom Burgin Date: Mon, 26 Oct 2026 09:43:26 -0400 Subject: [PATCH 1/2] Added a No-Issue escape hatch to the linked-issue check A pull request without a Linear key now passes when its body has a "No-Issue: " line. The check shows the reason as a notice. --- .github/scripts/linked-issue.js | 11 ++++++++++- .github/scripts/linked-issue.test.js | 10 +++++++++- .github/workflows/linked-issue.yml | 10 +++++++--- README.md | 4 +++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/scripts/linked-issue.js b/.github/scripts/linked-issue.js index 6fb97fc..990e6b1 100644 --- a/.github/scripts/linked-issue.js +++ b/.github/scripts/linked-issue.js @@ -2,9 +2,18 @@ // team keys because this file is public. It skips common non-issue tokens. const KEY = /\b(?!(?:SHA|UTF|ISO|CVE|RFC|TLS)-)[A-Z]{2,5}-\d+\b/i; +// A body line such as "No-Issue: typo fix" explains why the PR has no issue. +const NO_ISSUE = /^No-Issue:[ \t]*(\S.*)$/im; + // join() turns a null body into an empty string. function hasLinearKey(title, body, branch) { return KEY.test([title, body, branch].join('\n')); } -module.exports = { hasLinearKey }; +// Returns the No-Issue reason from the body, or null if there is none. +function noIssueReason(body) { + const match = NO_ISSUE.exec(body ?? ''); + return match ? match[1].trim() : null; +} + +module.exports = { hasLinearKey, noIssueReason }; diff --git a/.github/scripts/linked-issue.test.js b/.github/scripts/linked-issue.test.js index 827d949..b750c0b 100644 --- a/.github/scripts/linked-issue.test.js +++ b/.github/scripts/linked-issue.test.js @@ -1,5 +1,5 @@ const assert = require('node:assert/strict'); -const { hasLinearKey } = require('./linked-issue.js'); +const { hasLinearKey, noIssueReason } = require('./linked-issue.js'); assert.ok(hasLinearKey('Fix login', '', 'user/abc-42'), 'lowercase branch key'); assert.ok(hasLinearKey('Fix login', 'Fixes ABC-123', 'user/fix-login'), 'key in body'); @@ -8,4 +8,12 @@ assert.ok(!hasLinearKey('Fix login', null, 'user/fix-login'), 'empty body, no ke for (const s of ['UTF-8', 'ISO-8601', 'CVE-2024-1234', 'RFC-9110', 'TLS-1']) { assert.ok(!hasLinearKey(s, '', 'main'), `${s} is not a key`); } + +assert.equal(noIssueReason('Summary\n\nNo-Issue: typo fix\r\n'), 'typo fix', 'reason on its own line'); +assert.equal(noIssueReason('no-issue: bump CI cache '), 'bump CI cache', 'case and spaces'); +assert.equal(noIssueReason('No-Issue:'), null, 'empty reason'); +assert.equal(noIssueReason('No-Issue: \nnext line'), null, 'blank reason does not span lines'); +assert.equal(noIssueReason('See No-Issue: below'), null, 'must start a line'); +assert.equal(noIssueReason(''), null, 'template comment'); +assert.equal(noIssueReason(null), null, 'null body'); console.log('ok'); diff --git a/.github/workflows/linked-issue.yml b/.github/workflows/linked-issue.yml index 4881bfe..5ab5b73 100644 --- a/.github/workflows/linked-issue.yml +++ b/.github/workflows/linked-issue.yml @@ -29,7 +29,11 @@ jobs: pull_number: context.payload.pull_request.number, }); if (pr.user.login === 'dependabot[bot]') return; - const { hasLinearKey } = require(`${process.env.GITHUB_WORKSPACE}/central/.github/scripts/linked-issue.js`); - if (!hasLinearKey(pr.title, pr.body, pr.head.ref)) { - core.setFailed('Add a Linear issue key, for example ABC-123, to the PR title, body, or branch name.'); + const { hasLinearKey, noIssueReason } = require(`${process.env.GITHUB_WORKSPACE}/central/.github/scripts/linked-issue.js`); + if (hasLinearKey(pr.title, pr.body, pr.head.ref)) return; + const reason = noIssueReason(pr.body); + if (reason) { + core.notice(`No linked issue: ${reason}`); + return; } + core.setFailed('Add a Linear issue key, for example ABC-123, to the PR title, body, or branch name. Or add a "No-Issue: " line to the body.'); diff --git a/README.md b/README.md index 9956809..b6de5ed 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ The `linked-issue` check fails when a pull request has no Linear issue key. A ke The check does not count common tokens such as `SHA-256`, `UTF-8`, and `CVE-2024-1234` as keys. Pull requests from Dependabot pass. -If the check fails, add the key to the title or the body. Then push a commit or run the check again. GitHub does not run the check again when you only edit the title or the body. +If a pull request has no issue, add a line to the body that starts with `No-Issue:` and gives the reason, for example `No-Issue: fix a typo in the README`. The check passes and shows the reason as a notice. A `No-Issue:` line with no reason does not count. + +If the check fails, add the key to the title or the body, or add a `No-Issue:` line to the body. Then push a commit or run the check again. GitHub does not run the check again when you only edit the title or the body. ## Test From 830041b60b38339a5aa0780e826d66c81825139b Mon Sep 17 00:00:00 2001 From: Tom Burgin Date: Mon, 26 Oct 2026 09:47:43 -0400 Subject: [PATCH 2/2] Ignored No-Issue lines inside HTML comments PR templates often hold hints in HTML comments, which do not render. A No-Issue line inside one, including a multiline or unclosed comment, no longer passes the linked-issue check. --- .github/scripts/linked-issue.js | 5 ++++- .github/scripts/linked-issue.test.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/scripts/linked-issue.js b/.github/scripts/linked-issue.js index 990e6b1..f854d66 100644 --- a/.github/scripts/linked-issue.js +++ b/.github/scripts/linked-issue.js @@ -5,6 +5,9 @@ const KEY = /\b(?!(?:SHA|UTF|ISO|CVE|RFC|TLS)-)[A-Z]{2,5}-\d+\b/i; // A body line such as "No-Issue: typo fix" explains why the PR has no issue. const NO_ISSUE = /^No-Issue:[ \t]*(\S.*)$/im; +// HTML comments, such as PR template hints, do not render. An unclosed one hides the rest. +const HTML_COMMENT = /|$)/g; + // join() turns a null body into an empty string. function hasLinearKey(title, body, branch) { return KEY.test([title, body, branch].join('\n')); @@ -12,7 +15,7 @@ function hasLinearKey(title, body, branch) { // Returns the No-Issue reason from the body, or null if there is none. function noIssueReason(body) { - const match = NO_ISSUE.exec(body ?? ''); + const match = NO_ISSUE.exec((body ?? '').replace(HTML_COMMENT, '')); return match ? match[1].trim() : null; } diff --git a/.github/scripts/linked-issue.test.js b/.github/scripts/linked-issue.test.js index b750c0b..7d0d9e6 100644 --- a/.github/scripts/linked-issue.test.js +++ b/.github/scripts/linked-issue.test.js @@ -15,5 +15,8 @@ assert.equal(noIssueReason('No-Issue:'), null, 'empty reason'); assert.equal(noIssueReason('No-Issue: \nnext line'), null, 'blank reason does not span lines'); assert.equal(noIssueReason('See No-Issue: below'), null, 'must start a line'); assert.equal(noIssueReason(''), null, 'template comment'); +assert.equal(noIssueReason(''), null, 'multiline template comment'); +assert.equal(noIssueReason('\nNo-Issue: real reason'), 'real reason', 'reason after a comment'); assert.equal(noIssueReason(null), null, 'null body'); console.log('ok');