Skip to content
25 changes: 23 additions & 2 deletions .github/workflows/pull-request-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ on:
branches:
- 'gh-pages'

permissions:
contents: read

jobs:
Check-For-Linked-Issue:
runs-on: ubuntu-latest
if: ${{ github.event.action == 'opened' || github.event.action == 'edited' }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @castillios! This work looks really good. I did have one question about the event actions we'd want to consider here and on line 2-4:

on:
  pull_request:
    types: [opened, edited]

Do we want to include "reopened" as well?

Here are the docs I was looking at where I got this idea: https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2026-03-10#event-payload-object-for-issuesevent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meghan-oconnell I do think it would be a good idea to include reopened too.

env:
ARTIFACT_DIR: './pull-request-trigger/artifacts'
ARTIFACT_FILE: 'pr-comment.json'
SCRIPT_PATH: './github-actions/trigger-pr/check-linked-issue.js'
steps:
- name: Checkout repository
uses: actions/checkout@v7
Expand All @@ -18,5 +25,19 @@ jobs:
uses: actions/github-script@v9
with:
script: |
const script = require('./github-actions/trigger-pr/check-linked-issue.js')
script({g: github, c: context})
const { checkForLinkedIssue } = require('${{ env.SCRIPT_PATH }}');
return await checkForLinkedIssue({ github, context, core });
- name: Save PR comment
id: save-pr-comment
run: |
mkdir -p '${{ env.ARTIFACT_DIR }}'
echo ${{ steps.check-for-keyword.outputs.result }} > '${{ env.ARTIFACT_DIR }}/${{ env.ARTIFACT_FILE }}'

- name: Upload PR comment artifact
id: upload-pr-comment-artifact
uses: actions/upload-artifact@v7
with:
name: pr-comment
path: '${{ env.ARTIFACT_DIR }}/${{ env.ARTIFACT_FILE }}'
retention-days: 1

29 changes: 27 additions & 2 deletions .github/workflows/wr-pull-request-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,33 @@ on:
workflows: ["Pull Request Trigger"]
types: [completed]

permissions:
contents: read
issues: write
pull-requests: write

jobs:
Hello-World:
post-pr-comment:
runs-on: ubuntu-latest
env:
ARTIFACT_NAME: 'pr-comment'
SCRIPT_PATH: './github-actions/trigger-pr/check-linked-issue.js'
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- uses: actions/checkout@v7
- name: Download PR comment artifact
id: download-pr-comment-artifact
uses: actions/github-script@v9
with:
script: |
const { downloadPRCommentArtifact } = require('${{ env.SCRIPT_PATH }}');
await downloadPRCommentArtifact({ github, context }, '${{ env.ARTIFACT_NAME }}');
- name: Extract artifact
id: extract-artifact
run: unzip '${{ env.ARTIFACT_NAME }}.zip'
- name: Post comment to PR
id: post-comment-to-pr
uses: actions/github-script@v9
with:
script: |
const { postPRComment } = require('${{ env.SCRIPT_PATH }}');
await postPRComment({ github, context, core }, '${{ env.ARTIFACT_NAME }}.json');
188 changes: 158 additions & 30 deletions github-actions/trigger-pr/check-linked-issue.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,182 @@
// Import modules
const fs = require('fs');
const postIssueComment = require('../utils/post-issue-comment');

// Global variables
var github;
var context;
/** **************************************
** HELPER FUNCTIONS
*************************************** */

async function main({ g, c }) {
github = g;
context = c;
/**
* Returns resulting Object from the GitHub API downloadArtifact call.
* This function is a wrapper for the downloadArtifact method in the
* octokit/rest.js client.
* https://octokit.github.io/rest.js/v18#actions-download-artifact
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @param {Object} artifact - The artifact
* @returns {Object} The resulting Object
*/
async function _downloadArtifact({github, context}, artifact) {
const { owner, repo } = context.repo;
const { id: artifact_id } = artifact;
return await github.rest.actions.downloadArtifact({
owner,
repo,
artifact_id,
archive_format: 'zip',
});
}

/**
* Returns the resulting Object from the GitHub API listWorkflowRunArtifacts
* call.
* This function is a wrapper for the listWorkflowRunArtifacts method in the
* octokit/rest.js client.
* https://octokit.github.io/rest.js/v18#actions-list-workflow-run-artifacts
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @returns {Object} The resulting Object
*/
async function _listWorkflowRunArtifacts({ github, context }) {
const { owner, repo } = context.repo;
const { id: run_id } = context.payload.workflow_run;
return await github.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id,
});
}

// Retrieve body of context.payload and search for GitHub keywords followed by
// '#' + number. Exclude any matches that are in a comment within the PR body
/**
* Returns PR data containing the body, PR number, and owner
* by retrieving data from context.payload
* @param {Object} packages.context - The context of the workflow run
* @param {Object} packages.core - The @actions/core package
* @returns {Object} An object containing the pull request body, number and owner.
*/
function _retrievePRBody({ context, core }) {
const prBody = context.payload.pull_request.body;
const prNumber = context.payload.pull_request.number;
const prOwner = context.payload.pull_request.user.login;

core.info(`Found PR #${prNumber} authored by ${prOwner}`);


return { body: prBody, number: prNumber, owner: prOwner };
}


/**
* Returns the resulting Object from the GitHub API get issue call.
* This function servers as a wrapper for the get issue method in the
* octokit.rest.js client.
* https://octokit.github.io/rest.js/v20#issues-get
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @param {string} issueNum - The issue number to be looked up
* @returns
*/
async function _checkIssueExists({ github, context }, issueNum) {
return await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNum,
});
}

/** **************************************
** MAIN FUNCTIONS
*************************************** */

/**
* Returns a PR comment if issue is not linked or linked issue could not be found.
* Otherwise, returns empty PR comment, indicating that the issue has been properly linked.
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @param {Object} packages.core - The @actions/core package
* @returns
*/
async function checkForLinkedIssue({ github, context, core }) {
const pr = _retrievePRBody({ context, core });

// Search for GitHub keywords followed by
// '#' + number. Exclude any matches that are in a comment within the PR body.
const regex = /(?!<!--)(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*#(\d+)(?![^<]*-->)/gi;
const match = prBody.match(regex);
let prComment;
const match = pr.body.match(regex);

@meghan-oconnell meghan-oconnell Oct 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @castillios ! Thank you for your patience as I reviewed this. Your code is looking great. I tested it and mostly saw successes, the artifact made, and the comment made on the PR when appropriate.

There was one thing. It looks like this was already an issue in the code before, but in testing this, I noticed that when you submit with a blank PR message, it fails altogether.

Image

I will link the fails and include the error message I am seeing:

To fix this, something like this might work:
const match = (pr.body ?? '').match(regex);
(though I haven't tested it)

Maybe we assume people will put something in that PR, especially since there is a template that auto populates, so it would't really come up. Let me know what you think there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meghan-oconnell this is a good point, it's probably rare someone would submit a blank PR but the code should handle it if it does happen.


let prComment = "";

if (!match) {
console.log('PR does not have a properly linked issue. Posting comment...');
prComment = `@${prOwner}, this Pull Request is not linked to a valid issue. Above, on the first line of your PR, please link the number of the issue that you worked on using the format of 'Fixes #' + issue number, for example: **_Fixes #9876_**\n\nNote: Do **_not_** use the number of this PR.`;
}

else {
console.log(match[0]);
core.info('PR does not have a properly linked issue. Posting comment...');
prComment = `@${pr.owner}, this Pull Request is not linked to a valid issue. Above, on the first line of your PR, please link the number of the issue that you worked on using the format of 'Fixes #' + issue number, for example: **_Fixes #9876_**\n\nNote: Do **_not_** use the number of this PR.`;
} else {
core.info(match[0]);
let [ keyword, linkNumber ] = match[0].replaceAll('#','').split(' ');
console.log(`Found a keyword: \'${keyword}\'. Checking for legitimate linked issue...`);

core.info(`Found a keyword: \'${keyword}\'. Checking for legitimate linked issue...`);

// Check if the linked issue exists in repo
// https://octokit.github.io/rest.js/v20#issues-get
try {
await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: linkNumber,
});
console.log(`Found an issue: \'#${linkNumber}\' in repo. Reference is a legitimate linked issue.`);
await _checkIssueExists({ github, context }, linkNumber);
core.info(`Found an issue: \'#${linkNumber}\' in repo. Reference is a legitimate linked issue.`);
}
catch (error) {
console.log(`Couldn\'t find issue: \'#${linkNumber}\' in repo. Posting comment...`);
prComment = `@${prOwner}, the issue number referenced above as "**${keyword} #${linkNumber}**" is not found. Please replace with a valid issue number.`;
core.info(`Couldn\'t find issue: \'#${linkNumber}\' in repo. Posting comment...`);
prComment = `@${pr.owner}, the issue number referenced above as "**${keyword} #${linkNumber}**" is not found. Please replace with a valid issue number.`;
}
}

// If the prComment was given text, then post the comment to the PR
if (prComment) {
return JSON.stringify({ prNumber: pr.number, prComment});
}

/**
* Returns the file path of the downloaded artifact.
* The function downloads the artifact and stores it in a pre-determined
* location.
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @param {Object} packages.core - The @actions/core package
* @param {string} artifactName - The name of the artifact
* @returns {string} The file path of the downloaded artifact
*/
async function downloadPRCommentArtifact({ github, context }, artifactName) {
const artifacts = await _listWorkflowRunArtifacts({ github, context });
const match = artifacts.data.artifacts.filter(
({ name }) => name === artifactName
)[0];

const download = await _downloadArtifact({github, context }, match);

const filepath = `${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`;
fs.writeFileSync(filepath, Buffer.from(download.data));
return filepath;
}

/**
* Calls postIssueComment to post a comment to the PR on GitHub
* This function us a wrapper for the postIssueComment method
* and will only be called if prComment contains text.
* @param {Object} packages.github - The octokit/rest.js client
* @param {Object} packages.context - The context of the workflow run
* @param {string} filepath - The path of the file
*/

async function postPRComment({ github, context, core }, filepath) {
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
const { prNumber, prComment } = data;

if (prComment){
core.info(`Posting PR comment...`)
postIssueComment(prNumber, prComment, github, context);
core.info(`Posted comment to PR #${prNumber}:`)
core.info(JSON.stringify(prComment));
} else {
core.info(`No comment posted. Issue is properly linked.`)
}
}

module.exports = main;
module.exports = {
downloadPRCommentArtifact,
checkForLinkedIssue,
postPRComment,
};
Loading