diff --git a/.github/scripts/linked-issue.js b/.github/scripts/linked-issue.js index 26626fa..4be0f19 100644 --- a/.github/scripts/linked-issue.js +++ b/.github/scripts/linked-issue.js @@ -1,6 +1,12 @@ // Matches the issue keys of our Linear teams, in any case. const KEY = /\b(?:[KW][MPS]|[FINRSZ][AELMNP][FLQRSTWX])-\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,4 +18,10 @@ function isBot(user) { return user.type === 'Bot'; } -module.exports = { hasLinearKey, isBot }; +// Returns the No-Issue reason from the body, or null if there is none. +function noIssueReason(body) { + const match = NO_ISSUE.exec((body ?? '').replace(HTML_COMMENT, '')); + return match ? match[1].trim() : null; +} + +module.exports = { hasLinearKey, isBot, noIssueReason }; diff --git a/.github/scripts/linked-issue.test.js b/.github/scripts/linked-issue.test.js index f7bd0db..7af6d23 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, isBot } = require('./linked-issue.js'); +const { hasLinearKey, isBot, noIssueReason } = require('./linked-issue.js'); assert.ok(hasLinearKey('Fix login', '', 'user/ws-42'), 'lowercase branch key'); assert.ok(hasLinearKey('Fix login', 'Fixes WS-123', 'user/fix-login'), 'key in body'); @@ -16,4 +16,15 @@ for (const s of ['SHA-256', 'UTF-8', 'ISO-8601', 'CVE-2024-1234', 'RFC-9110', 'T assert.ok(isBot({ login: 'dependabot[bot]', type: 'Bot' }), 'Dependabot is a bot'); assert.ok(isBot({ login: 'nps-schema-doc-generator[bot]', type: 'Bot' }), 'app bot'); assert.ok(!isBot({ login: 'someone', type: 'User' }), 'a person is not a bot'); + +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, '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'); diff --git a/.github/workflows/linked-issue.yml b/.github/workflows/linked-issue.yml index afc7968..7a5b9fb 100644 --- a/.github/workflows/linked-issue.yml +++ b/.github/workflows/linked-issue.yml @@ -28,8 +28,12 @@ jobs: ...context.repo, pull_number: context.payload.pull_request.number, }); - const { hasLinearKey, isBot } = require(`${process.env.GITHUB_WORKSPACE}/central/.github/scripts/linked-issue.js`); + const { hasLinearKey, isBot, noIssueReason } = require(`${process.env.GITHUB_WORKSPACE}/central/.github/scripts/linked-issue.js`); if (isBot(pr.user)) return; - if (!hasLinearKey(pr.title, pr.body, pr.head.ref)) { - core.setFailed('Add a Linear issue key to the PR title, body, or branch name.'); + 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 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 0e6d21f..4bb1285 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, for Pull requests from GitHub App bots, such as 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