Skip to content

Commit 65cd906

Browse files
authored
feat: support auto commenting on closed linked issues after release (#50)
ci: support auto commenting on closed linked issues after release - Refactored `findClosingKeywordReferences` utils function to optionally match already linked references via Markdown links with a new `matchMarkdownLinks` param. - Added new step in the publish CI to comment on closed issues that are referenced in the changelog of the newly released version. This step takes place after the publishing step, and uses the new script file. - Added new `comment-on-linked-issues` script file for the functionality of finding any reference numbers in the changelog and auto comment on the issues that the new release has been published. The script has: - `commentOnLinkedIssues` main function to retrieve the closing issues from the changelog version entry, and add comment on them to let the issue author know that the resolution of the issues has been released in a new version. This uses the `findClosingKeywordReferences` utils function to retrieve the issue-closing keyword references using the new `matchMarkdownLinks` param as `true`. The script also uses the Octokit GitHub API to create the comment. - `extractChangelogEntry` function to extract the changelog section for a specified version. - `commentExists` function to check if a comment already exists for the specific version on the issue. This uses the Octokit GitHub API to paginate through the issue's comments and filters the comments that match the criteria. - Updated permissions to allow writing issues in the publish CI.
1 parent ad2fa06 commit 65cd906

3 files changed

Lines changed: 160 additions & 7 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Comments on closed issues referenced (directly, or indirectly via a linked PR)
3+
* in a version's changelog entry. Used by the publish workflow after a release deploy.
4+
*/
5+
6+
import {readFileSync} from "fs";
7+
import * as utils from "./utils.mjs";
8+
9+
/**
10+
* Main function to comment on closed issues referenced in a version's changelog entry.
11+
*
12+
* @param {object} params
13+
* @param {import('@actions/github-script').AsyncFunctionArguments["github"]} params.github Octokit instance
14+
* @param {import('@actions/github-script').AsyncFunctionArguments["context"]} params.context Workflow run context
15+
* @param {string} params.version Released version (without leading "v")
16+
*/
17+
export default async function commentOnLinkedIssues({github, context, version}) {
18+
const owner = context.repo.owner;
19+
const repo = context.repo.repo;
20+
const releaseUrl = context.payload.release.html_url;
21+
22+
// Read the changelog.
23+
const changelog = readFileSync("CHANGELOG.md", "utf8");
24+
// Extract the changelog entry for the released version.
25+
const entry = extractChangelogEntry(changelog, version);
26+
27+
// Find all closing keyword issue references.
28+
const issuesToComment = utils.findClosingKeywordReferences(entry, true);
29+
30+
console.log("Issues to comment on:", [...issuesToComment]);
31+
32+
// Loop through each issue number and comment on it with a message about the release.
33+
for (const issueNumber of issuesToComment) {
34+
const comment = `🚀 This issue has been resolved and released in [v${version}](${releaseUrl})! Please update to v${version}.`;
35+
36+
// If a comment already exists for the version, skip commenting on this issue.
37+
if (await commentExists(github, context, issueNumber, `${comment}`)) {
38+
console.log(`Comment already exists on issue #${issueNumber}, skipping.`);
39+
continue;
40+
}
41+
42+
// Create a comment on the issue.
43+
await github.rest.issues.createComment({
44+
owner,
45+
repo,
46+
issue_number: issueNumber,
47+
body: comment,
48+
});
49+
console.log(`Commented on issue #${issueNumber}`);
50+
}
51+
}
52+
53+
/**
54+
* Extracts the changelog section for a given version
55+
*
56+
* @param {string} changelog Full CHANGELOG.md content
57+
* @param {string} version Version to find (without leading "v")
58+
* @returns {string} The changelog entry text for that version
59+
*/
60+
function extractChangelogEntry(changelog, version) {
61+
// Find the index of the version heading in the changelog.
62+
const versionHeadingIndex = changelog.indexOf(`## [${version}]`);
63+
64+
// If the version heading is not found, throw an error.
65+
if (versionHeadingIndex === -1) {
66+
throw new Error(`Could not find CHANGELOG.md entry for version ${version}`);
67+
}
68+
69+
// Find the index of the next version heading (or end of file)
70+
const nextHeadingIndex = changelog.indexOf("\n## [", versionHeadingIndex + 1);
71+
72+
// Return the full version entry section in the changelog. The section starts with the
73+
// version heading and continues until the next version heading or the end of the file.
74+
return nextHeadingIndex === -1 ? changelog.slice(versionHeadingIndex) : changelog.slice(versionHeadingIndex, nextHeadingIndex);
75+
}
76+
77+
/**
78+
* Checks if a comment with the given substring already exists on the issue.
79+
*
80+
* @param {import('@actions/github-script').AsyncFunctionArguments["github"]} github Octokit instance
81+
* @param {import('@actions/github-script').AsyncFunctionArguments["context"]} context GitHub Actions context
82+
* @param {number} issueNumber Issue number
83+
* @param {string} substring Substring to check for in the comment body
84+
* @returns {boolean} True if a comment containing the substring exists, false otherwise
85+
*/
86+
async function commentExists(github, context, issueNumber, substring) {
87+
// Paginate through all existing comments on the issue and
88+
// check if any comment contains the substring.
89+
const filteredArray = await github.paginate(
90+
github.rest.issues.listComments,
91+
{
92+
owner: context.repo.owner,
93+
repo: context.repo.repo,
94+
issue_number: issueNumber,
95+
},
96+
// A callback function is called for each page of comments and returns a
97+
// filtered array of comments that match the criteria.
98+
(response, done) => {
99+
// Find the comment containing the substring in the body.
100+
const foundComment = response.data.find((comment) => comment.body.includes(substring));
101+
102+
// If a comment is found, we can stop paginating and return the comment.
103+
if (foundComment) {
104+
done();
105+
return foundComment;
106+
}
107+
108+
// Otherwise, continue paginating through the comments until we find a match or
109+
// reach the end of the list. Return an empty array if no comment is found.
110+
return [];
111+
},
112+
);
113+
114+
console.log("Comments found:", filteredArray);
115+
116+
// If the filtered array contains any comments that
117+
// match the substring, return true, false otherwise.
118+
return filteredArray.length > 0 ? true : false;
119+
}

.github/scripts/utils.mjs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,42 @@ export const INCLUDED_TYPES = Object.keys(TYPE_TO_SECTION);
2121

2222
/**
2323
* Finds all closing keyword references from a given text. These are always issues.
24-
* Already linked references are ignored, as they don't need to be linkified.
25-
* E.g., "Closes #12", "Fixes #45", "Resolves #77" will all be matched,
26-
* but "[#13](...)" will not be matched.
24+
*
25+
* If `matchMarkdownLinks` is `true`, then it will match already linked references via
26+
* Markdown links e.g., `[#123](...)`, otherwise it will ignore them.
27+
*
28+
* Example:
29+
* - `matchMarkdownLinks=false`: `"Closes #12"` will be matched, but `"Closes [#13](...)"`
30+
* will not be matched.
31+
* - `matchMarkdownLinks=true`: `"Closes [#13](...)"` will be matched, but `"Closes #12"`
32+
* will not be matched.
2733
*
2834
* @param {string} text Text to search for closing keyword references
35+
*
36+
* @param {boolean} [matchMarkdownLinks=false] Whether to match already linked references via Markdown links e.g., `[#123](...)`. If `false` (default), it won't match Markdown links.
2937
* @returns {Set<string>} Set of referenced issue numbers
3038
*/
31-
function findClosingKeywordReferences(text) {
39+
export function findClosingKeywordReferences(text, matchMarkdownLinks = false) {
3240
// Collect the bare reference numbers in a Set to ensure it only captures unique numbers.
3341
const closingNumbers = new Set();
3442
let match;
43+
// 1st part of the regex to match issue-closing keyword references.
44+
let regex = "\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*";
3545

36-
// Regex to match issue-closing keyword references. The (?!\]) negative lookahead ensures
37-
// it doesn't match references that are already linked (e.g., [#123](...)).
38-
const regex = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*#(\d+)\b(?!\])/gi;
46+
// If matchMarkdownLinks is false, DON'T match already linked references.
47+
if (!matchMarkdownLinks) {
48+
// The (?!\]) negative lookahead ensures it DOESN'T match references
49+
// that are already linked (e.g., closes #12).
50+
regex = new RegExp(regex + "#(\\d+)\\b(?!\\])", "gi");
51+
}
52+
// Otherwise, match references that are already linked.
53+
else {
54+
// Matches already linked references (e.g., closes [#12](...)).
55+
regex = new RegExp(regex + "\\[#(\\d+)\\]\\(.*?\\)", "gi");
56+
}
3957

58+
// While there are matching issue-closing keyword references in the text,
59+
// add the reference numbers to the Set.
4060
while ((match = regex.exec(text)) !== null) {
4161
closingNumbers.add(match[1]);
4262
}

.github/workflows/publish-extension.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ permissions:
1212
contents: write
1313
pull-requests: write
1414
actions: read
15+
issues: write
1516

1617
name: Publish Extension to VS Code Marketplace and Open VSX Registry
1718
jobs:
@@ -333,3 +334,16 @@ jobs:
333334
- name: Report Success
334335
run: |
335336
echo "✅ Successfully published to both marketplaces"
337+
338+
- name: Comment on linked issues
339+
uses: actions/github-script@v7
340+
with:
341+
script: |
342+
const { default: commentOnLinkedIssues } = await import('${{ github.workspace }}/.github/scripts/comment-on-linked-issues.mjs');
343+
await commentOnLinkedIssues({
344+
github,
345+
context,
346+
version: '${{ needs.validate-release-version.outputs.version }}',
347+
});
348+
env:
349+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)