From 037eeb77b3604f5bc3a72169dc4701a16297a1aa Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Wed, 19 Aug 2026 05:28:43 +0100 Subject: [PATCH] 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. --- .github/scripts/comment-on-linked-issues.mjs | 119 +++++++++++++++++++ .github/scripts/utils.mjs | 34 ++++-- .github/workflows/publish-extension.yml | 14 +++ 3 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 .github/scripts/comment-on-linked-issues.mjs diff --git a/.github/scripts/comment-on-linked-issues.mjs b/.github/scripts/comment-on-linked-issues.mjs new file mode 100644 index 0000000..850406f --- /dev/null +++ b/.github/scripts/comment-on-linked-issues.mjs @@ -0,0 +1,119 @@ +/** + * Comments on closed issues referenced (directly, or indirectly via a linked PR) + * in a version's changelog entry. Used by the publish workflow after a release deploy. + */ + +import {readFileSync} from "fs"; +import * as utils from "./utils.mjs"; + +/** + * Main function to comment on closed issues referenced in a version's changelog entry. + * + * @param {object} params + * @param {import('@actions/github-script').AsyncFunctionArguments["github"]} params.github Octokit instance + * @param {import('@actions/github-script').AsyncFunctionArguments["context"]} params.context Workflow run context + * @param {string} params.version Released version (without leading "v") + */ +export default async function commentOnLinkedIssues({github, context, version}) { + const owner = context.repo.owner; + const repo = context.repo.repo; + const releaseUrl = context.payload.release.html_url; + + // Read the changelog. + const changelog = readFileSync("CHANGELOG.md", "utf8"); + // Extract the changelog entry for the released version. + const entry = extractChangelogEntry(changelog, version); + + // Find all closing keyword issue references. + const issuesToComment = utils.findClosingKeywordReferences(entry, true); + + console.log("Issues to comment on:", [...issuesToComment]); + + // Loop through each issue number and comment on it with a message about the release. + for (const issueNumber of issuesToComment) { + const comment = `🚀 This issue has been resolved and released in [v${version}](${releaseUrl})! Please update to v${version}.`; + + // If a comment already exists for the version, skip commenting on this issue. + if (await commentExists(github, context, issueNumber, `${comment}`)) { + console.log(`Comment already exists on issue #${issueNumber}, skipping.`); + continue; + } + + // Create a comment on the issue. + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: comment, + }); + console.log(`Commented on issue #${issueNumber}`); + } +} + +/** + * Extracts the changelog section for a given version + * + * @param {string} changelog Full CHANGELOG.md content + * @param {string} version Version to find (without leading "v") + * @returns {string} The changelog entry text for that version + */ +function extractChangelogEntry(changelog, version) { + // Find the index of the version heading in the changelog. + const versionHeadingIndex = changelog.indexOf(`## [${version}]`); + + // If the version heading is not found, throw an error. + if (versionHeadingIndex === -1) { + throw new Error(`Could not find CHANGELOG.md entry for version ${version}`); + } + + // Find the index of the next version heading (or end of file) + const nextHeadingIndex = changelog.indexOf("\n## [", versionHeadingIndex + 1); + + // Return the full version entry section in the changelog. The section starts with the + // version heading and continues until the next version heading or the end of the file. + return nextHeadingIndex === -1 ? changelog.slice(versionHeadingIndex) : changelog.slice(versionHeadingIndex, nextHeadingIndex); +} + +/** + * Checks if a comment with the given substring already exists on the issue. + * + * @param {import('@actions/github-script').AsyncFunctionArguments["github"]} github Octokit instance + * @param {import('@actions/github-script').AsyncFunctionArguments["context"]} context GitHub Actions context + * @param {number} issueNumber Issue number + * @param {string} substring Substring to check for in the comment body + * @returns {boolean} True if a comment containing the substring exists, false otherwise + */ +async function commentExists(github, context, issueNumber, substring) { + // Paginate through all existing comments on the issue and + // check if any comment contains the substring. + const filteredArray = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }, + // A callback function is called for each page of comments and returns a + // filtered array of comments that match the criteria. + (response, done) => { + // Find the comment containing the substring in the body. + const foundComment = response.data.find((comment) => comment.body.includes(substring)); + + // If a comment is found, we can stop paginating and return the comment. + if (foundComment) { + done(); + return foundComment; + } + + // Otherwise, continue paginating through the comments until we find a match or + // reach the end of the list. Return an empty array if no comment is found. + return []; + }, + ); + + console.log("Comments found:", filteredArray); + + // If the filtered array contains any comments that + // match the substring, return true, false otherwise. + return filteredArray.length > 0 ? true : false; +} diff --git a/.github/scripts/utils.mjs b/.github/scripts/utils.mjs index cf06dd6..7c19b70 100644 --- a/.github/scripts/utils.mjs +++ b/.github/scripts/utils.mjs @@ -21,22 +21,42 @@ export const INCLUDED_TYPES = Object.keys(TYPE_TO_SECTION); /** * Finds all closing keyword references from a given text. These are always issues. - * Already linked references are ignored, as they don't need to be linkified. - * E.g., "Closes #12", "Fixes #45", "Resolves #77" will all be matched, - * but "[#13](...)" will not be matched. + * + * If `matchMarkdownLinks` is `true`, then it will match already linked references via + * Markdown links e.g., `[#123](...)`, otherwise it will ignore them. + * + * Example: + * - `matchMarkdownLinks=false`: `"Closes #12"` will be matched, but `"Closes [#13](...)"` + * will not be matched. + * - `matchMarkdownLinks=true`: `"Closes [#13](...)"` will be matched, but `"Closes #12"` + * will not be matched. * * @param {string} text Text to search for closing keyword references + * + * @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. * @returns {Set} Set of referenced issue numbers */ -function findClosingKeywordReferences(text) { +export function findClosingKeywordReferences(text, matchMarkdownLinks = false) { // Collect the bare reference numbers in a Set to ensure it only captures unique numbers. const closingNumbers = new Set(); let match; + // 1st part of the regex to match issue-closing keyword references. + let regex = "\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*"; - // Regex to match issue-closing keyword references. The (?!\]) negative lookahead ensures - // it doesn't match references that are already linked (e.g., [#123](...)). - const regex = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*#(\d+)\b(?!\])/gi; + // If matchMarkdownLinks is false, DON'T match already linked references. + if (!matchMarkdownLinks) { + // The (?!\]) negative lookahead ensures it DOESN'T match references + // that are already linked (e.g., closes #12). + regex = new RegExp(regex + "#(\\d+)\\b(?!\\])", "gi"); + } + // Otherwise, match references that are already linked. + else { + // Matches already linked references (e.g., closes [#12](...)). + regex = new RegExp(regex + "\\[#(\\d+)\\]\\(.*?\\)", "gi"); + } + // While there are matching issue-closing keyword references in the text, + // add the reference numbers to the Set. while ((match = regex.exec(text)) !== null) { closingNumbers.add(match[1]); } diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml index 4b788cd..b7ea86f 100644 --- a/.github/workflows/publish-extension.yml +++ b/.github/workflows/publish-extension.yml @@ -12,6 +12,7 @@ permissions: contents: write pull-requests: write actions: read + issues: write name: Publish Extension to VS Code Marketplace and Open VSX Registry jobs: @@ -333,3 +334,16 @@ jobs: - name: Report Success run: | echo "✅ Successfully published to both marketplaces" + + - name: Comment on linked issues + uses: actions/github-script@v7 + with: + script: | + const { default: commentOnLinkedIssues } = await import('${{ github.workspace }}/.github/scripts/comment-on-linked-issues.mjs'); + await commentOnLinkedIssues({ + github, + context, + version: '${{ needs.validate-release-version.outputs.version }}', + }); + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}