Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/scripts/comment-on-linked-issues.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 27 additions & 7 deletions .github/scripts/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} 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]);
}
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/publish-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Loading