Skip to content

Commit fbb8cc7

Browse files
committed
fix(ci): ignore HTML comments when validating linked issues
1 parent 0d95c39 commit fbb8cc7

4 files changed

Lines changed: 103 additions & 9 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
Closes #
1010

11-
<!-- Replace the # above with the issue number, e.g.: Closes #42 -->
11+
<!-- Replace the # above with the issue number, e.g.: Closes #<NUMBER> -->
1212

1313
---
1414

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
const assert = require('node:assert/strict');
4+
const test = require('node:test');
5+
6+
const {
7+
extractLinkedIssueNumbers,
8+
} = require('./validate-pr-issue-references.js');
9+
10+
test('extracts a single Closes #N reference', () => {
11+
assert.deepEqual(extractLinkedIssueNumbers('Closes #1770'), [1770]);
12+
});
13+
14+
test('strips a single-line HTML comment containing Closes #N', () => {
15+
const body = '<!-- Closes #42 -->\nCloses #1770';
16+
17+
assert.deepEqual(extractLinkedIssueNumbers(body), [1770]);
18+
});
19+
20+
test('strips a multi-line HTML comment', () => {
21+
const body = [
22+
'<!--',
23+
'Closes #1',
24+
'Fixes #2',
25+
'-->',
26+
'Resolves #1770',
27+
].join('\n');
28+
29+
assert.deepEqual(extractLinkedIssueNumbers(body), [1770]);
30+
});
31+
32+
test('preserves multiple visible references', () => {
33+
const body = 'Closes #1770\nResolves #123';
34+
35+
assert.deepEqual(extractLinkedIssueNumbers(body), [1770, 123]);
36+
});
37+
38+
test('returns empty array when only HTML comments present', () => {
39+
const body = '<!-- Closes #1 --><!-- Fixes #2\nResolves #3 -->';
40+
41+
assert.deepEqual(extractLinkedIssueNumbers(body), []);
42+
});
43+
44+
test('treats closing references in code fences as visible', () => {
45+
const body = '```markdown\nCloses #42\n```';
46+
47+
assert.deepEqual(extractLinkedIssueNumbers(body), [42]);
48+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const fs = require('node:fs');
4+
5+
const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
6+
const CLOSING_REFERENCE_PATTERN = /(?:closes|fixes|resolves)\s+#(\d+)/gi;
7+
8+
/**
9+
* Remove HTML comments from a Markdown string.
10+
*
11+
* @param {string} markdown
12+
* @returns {string}
13+
*/
14+
function stripHtmlComments(markdown) {
15+
return markdown.replace(HTML_COMMENT_PATTERN, '');
16+
}
17+
18+
/**
19+
* Extract issue numbers from visible closing references in a PR body.
20+
*
21+
* @param {string} body
22+
* @returns {number[]}
23+
*/
24+
function extractLinkedIssueNumbers(body) {
25+
const visibleBody = stripHtmlComments(body);
26+
27+
return [...visibleBody.matchAll(CLOSING_REFERENCE_PATTERN)].map((match) =>
28+
Number.parseInt(match[1], 10),
29+
);
30+
}
31+
32+
module.exports = {
33+
extractLinkedIssueNumbers,
34+
stripHtmlComments,
35+
};
36+
37+
if (require.main === module) {
38+
const body = process.argv.length > 2
39+
? process.argv.slice(2).join(' ')
40+
: fs.readFileSync(0, 'utf8');
41+
42+
process.stdout.write(`${JSON.stringify(extractLinkedIssueNumbers(body))}\n`);
43+
}

.github/workflows/pr-check.yml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,17 @@ jobs:
4646
name: Check Issue Reference
4747
runs-on: ubuntu-latest
4848
steps:
49+
- name: Check out repository
50+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
4951
- name: Verify PR body references an issue
5052
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
5153
with:
5254
script: |
55+
const { extractLinkedIssueNumbers } = require('./.github/scripts/validate-pr-issue-references.js');
5356
const body = context.payload.pull_request.body || '';
54-
const pattern = /(?:closes|fixes|resolves)\s+#(\d+)/gi;
55-
const matches = [...body.matchAll(pattern)];
57+
const issueNumbers = extractLinkedIssueNumbers(body);
5658
57-
if (matches.length === 0) {
59+
if (issueNumbers.length === 0) {
5860
core.setFailed(
5961
'❌ PR body must reference a linked issue using one of:\n' +
6062
' - Closes #<number>\n' +
@@ -63,7 +65,7 @@ jobs:
6365
'Every PR must be linked to an approved issue.'
6466
);
6567
} else {
66-
const numbers = matches.map(m => `#${m[1]}`).join(', ');
68+
const numbers = issueNumbers.map(issueNumber => `#${issueNumber}`).join(', ');
6769
console.log(`✅ Found issue reference(s): ${numbers}`);
6870
}
6971
@@ -72,21 +74,22 @@ jobs:
7274
runs-on: ubuntu-latest
7375
needs: check-issue-reference
7476
steps:
77+
- name: Check out repository
78+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
7579
- name: Verify linked issue(s) have status:approved label
7680
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
7781
with:
7882
github-token: ${{ secrets.GITHUB_TOKEN }}
7983
script: |
84+
const { extractLinkedIssueNumbers } = require('./.github/scripts/validate-pr-issue-references.js');
8085
const body = context.payload.pull_request.body || '';
81-
const pattern = /(?:closes|fixes|resolves)\s+#(\d+)/gi;
82-
const matches = [...body.matchAll(pattern)];
86+
const issueNumbers = extractLinkedIssueNumbers(body);
8387
84-
if (matches.length === 0) {
88+
if (issueNumbers.length === 0) {
8589
core.setFailed('❌ Could not find issue reference in PR body.');
8690
return;
8791
}
8892
89-
const issueNumbers = matches.map(m => parseInt(m[1], 10));
9093
const failures = [];
9194
9295
for (const issueNumber of issueNumbers) {

0 commit comments

Comments
 (0)