Skip to content
Closed
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' }}
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);

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