Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 3 additions & 2 deletions .github/workflows/activity-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jobs:
id: gather-event-details
uses: actions/github-script@v7
with:
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }}
# github-token defaults to `github.token` for 'pull_request_review'
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN != '' && secrets.HACKFORLA_GRAPHQL_TOKEN || github.token }}
script: |
const script = require('./github-actions/activity-trigger/activity-trigger.js');
const activities = script({github, context});
Expand All @@ -41,7 +42,7 @@ jobs:
id: post-to-skills-issue
uses: actions/github-script@v7
with:
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }}
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN != '' && secrets.HACKFORLA_GRAPHQL_TOKEN || github.token }}
script: |
const activities = JSON.parse(${{ steps.gather-event-details.outputs.result }});
const script = require('./github-actions/activity-trigger/post-to-skills-issue.js');
Expand Down
102 changes: 67 additions & 35 deletions github-actions/activity-trigger/activity-trigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,46 @@
async function activityTrigger({github, context}) {

let issueNum = '';
let assignee = '';
let timeline = '';

let eventName = context.eventName;
let eventAction = context.payload.action;
let eventActor = context.actor;

let eventObserver = '';
let eventPRAuthor = '';
let activities = [];

// Exclude all bot actors from being recorded as a guardrail against infinite loops
const EXCLUDED_ACTORS = ['HackforLABot', 'elizabethhonest', 'github-actions', 'github-advanced-security', 'github-pages', 'dependabot[bot]', 'dependabot-preview[bot]', 'dependabot', 'dependabot-preview'];
const EXCLUDED_ACTORS = [
"HackforLABot",
"elizabethhonest",
"dependabot",
"dependabot[bot]",
"github-actions",
"github-actions[bot]",
"github-advanced-security",
"github-advanced-security[bot]"
];

if (eventName === 'issues') {
issueNum = context.payload.issue.number;
eventUrl = context.payload.issue.html_url;
timeline = context.payload.issue.updated_at;
// If issue action is not opened and an assignee exists, then change
// the eventActor to the issue assignee, else retain issue author
assignee = context.payload.assignee?.login;
if (eventAction != 'opened' && assignee != null ) {
console.log(`Issue is ${eventAction}. Change eventActor => ${assignee}`);
eventActor = assignee;
} else {
eventActor = context.payload.issue.user.login;
}
// eventActor is the actor that directly causes or performs the eventAction
// eventObserver is the actor whose issue is being acted upon
if (eventAction === 'closed') {
// eventObserver is the assignee if exists, else is the issueAuthor
if (context.payload.issue.assignees?.length > 0) {
eventObserver = context.payload.issue.assignees[0].login; // aka assignee
} else {
eventObserver = context.payload.issue.user.login; // aka issueAuthor
}
let reason = context.payload.issue.state_reason;
eventActor = context.payload.issue.user.login;
eventAction = 'Closed-' + reason;
// eventActor is the assignee when eventAction is assigned/unassigned
} else if (eventAction === 'assigned' || eventAction === 'unassigned') {
eventActor = context.payload.assignee.login;
}
} else if (eventName === 'issue_comment') {
// Check if the comment is on an issue or a pull request
Expand All @@ -61,6 +72,11 @@ async function activityTrigger({github, context}) {
issueNum = context.payload.pull_request.number;
eventUrl = context.payload.review.html_url;
timeline = context.payload.review.updated_at;
eventActor = context.payload.review.user.login;
} else if (eventName === 'pull_request_review_comment') {
issueNum = context.payload.pull_request.number;
eventUrl = context.payload.comment.html_url;
timeline = context.payload.comment.updated_at;
}

// Return immediately if the issueNum is a Skills Issue- to discourage
Expand All @@ -81,37 +97,29 @@ async function activityTrigger({github, context}) {
'issues.assigned': 'assigned',
'issues.unassigned': 'unassigned',
'issue_comment.created': 'commented',
'pull_request_review.created': 'submitted review',
'pull_request_review.submitted': 'submitted review',
'pull_request_review_comment.created': 'commented',
'pull_request_comment.created': 'commented',
'pull_request.opened': 'opened',
'pull_request.PRclosed': 'closed',
'pull_request.PRmerged': 'merged',
'pull_request.reopened': 'reopened'
'pull_request_target.opened': 'opened',
'pull_request_target.PRclosed': 'closed',
'pull_request_target.PRmerged': 'merged',
'pull_request_target.reopened': 'reopened'
};

let localTime = getDateTime(timeline);
let action = actionMap[`${eventName}.${eventAction}`];
let message = `- ${eventActor} ${action}: ${eventUrl} at ${localTime}`;

// Check to confirm the eventActor isn't a bot
const isExcluded = (eventActor) => EXCLUDED_ACTORS.includes(eventActor);
if (!isExcluded(eventActor)) {
console.log(`Not a bot. Message to post: ${message}`);
activities.push([eventActor, message]);
}

// Only if issue is closed, and eventActor != assignee, return assignee and message
if (eventAction.includes('Closed-') && (eventActor !== assignee)) {
message = `- ${assignee} issue ${action}: ${eventUrl} at ${localTime}`;
activities.push([assignee, message]);
if (!checkIfBot(eventActor)) {
composeAndPushMessage(eventActor, action, eventUrl, localTime);
}
// Only if issue is closed, eventObserver !== eventActor, and eventObserver not a bot
if (eventAction.includes('Closed-') && (eventActor !== eventObserver) && (!checkIfBot(eventObserver))) {
composeAndPushMessage(eventObserver, `issue was ${action}`, eventUrl, localTime);
}
// Only if PRclosed or PRmerged, and PRAuthor != eventActor, return PRAuthor and message
if ((eventAction === 'PRclosed' || eventAction === 'PRmerged') && (eventActor != eventPRAuthor)) {
let messagePRAuthor = `- ${eventPRAuthor} PR was ${action}: ${eventUrl} at ${localTime}`;
if (!isExcluded(eventPRAuthor)) {
console.log(`Not a bot. Message to post: ${messagePRAuthor}`);
activities.push([eventPRAuthor, messagePRAuthor]);
}
if (eventAction.includes('PR') && (eventActor != eventPRAuthor) && (!checkIfBot(eventPRAuthor))) {
composeAndPushMessage(eventPRAuthor, `PR was ${action}`, eventUrl, localTime);
}

return JSON.stringify(activities);
Expand Down Expand Up @@ -147,6 +155,30 @@ async function activityTrigger({github, context}) {
return date.toLocaleString('en-US', options);
}

/**
* Helper function to check if eventActor is a bot
* @param {String} eventActor - the eventActor to check
* @returns {Boolean} - true if bot, false if not
*/
function checkIfBot(eventActor) {
let isBot = EXCLUDED_ACTORS.includes(eventActor);
if (isBot) console.log(`eventActor: ${eventActor} likely a bot. Do not post`);
return isBot;
}

/**
* Helper function to create message and push to activities array
* @param {String} actor - the eventActor
* @param {String} action - the action performed by the eventActor
* @param {String} url - the URL of the issue or PR
* @param {String} time - the date and time of the event
*/
function composeAndPushMessage(actor, action, url, time) {
let message = `- ${actor} ${action}: ${url} at ${time}`;
console.log(`Message to post: "${message}"`);
activities.push([actor, message]);
}

}

module.exports = activityTrigger;
module.exports = activityTrigger;
66 changes: 38 additions & 28 deletions github-actions/activity-trigger/post-to-skills-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,22 @@

// If eventActor undefined, exit
if (!eventActor) {
console.log(`eventActor is undefined (likely a bot). Cannot post message.`);
console.log(`eventActor is undefined (likely a bot). Cannot post message...`);
return;
}

// Get eventActor's Skills Issue number, nodeId, current statusId (all null if no Skills Issue found)
const skillsInfo = await querySkillsIssue(github, context, eventActor, SKILLS_LABEL);
const skillsInfo = await querySkillsIssue(github, context, eventActor, SKILLS_LABEL, isArchived);
const skillsIssueNum = skillsInfo.issueNum;
const skillsIssueNodeId = skillsInfo.issueId;
const skillsStatusId = skillsInfo.statusId;
const isArchived = skillsInfo.isArchived;

// Return immediately if Skills Issue not found
if (skillsIssueNum) {
console.log(`Found Skills Issue for ${eventActor}: #${skillsIssueNum}`);
console.log(`Found Skills Issue for ${eventActor}: #${skillsIssueNum}`);
} else {
console.log(`Did not find Skills Issue for ${eventActor}. Cannot post message.`);
console.log(`Did not find Skills Issue for ${eventActor}. Cannot post message.`);
return;
}

Expand All @@ -59,7 +60,7 @@
issue_number: skillsIssueNum,
});
} catch (err) {
console.error(`GET comments failed for issue #${skillsIssueNum}:`, err);
console.error(`GET comments failed for issue #${skillsIssueNum}:`, err);
return;
}

Expand All @@ -68,7 +69,7 @@
const commentFoundId = commentFound ? commentFound.id : null;

if (commentFound) {
console.log(`Found comment with MARKER: ${MARKER}`);
console.log(`Found comment with MARKER...`);
const commentId = commentFoundId;
const originalBody = commentFound.body;
const updatedBody = `${originalBody}\n${message}`;
Expand All @@ -80,37 +81,46 @@
commentId,
body: updatedBody
});
console.log(`Success! Entry posted to Skills Issue #${skillsIssueNum}`);
} catch (err) {
console.error(`Something went wrong updating comment:`, err);
console.error(`Something went wrong posting entry:`, err);
}

} else {
console.log(`MARKER not found in comments, creating new comment with MARKER...`);
console.log(`MARKER not found, creating new comment entry with MARKER...`);
const body = `${MARKER}\n## Activity Log: ${eventActor}\n### Repo: https://github.com/hackforla/website\n\n##### ⚠ Important note: The bot updates this comment automatically - do not edit\n\n${message}`;
await postComment(skillsIssueNum, body, github, context);
const commentPosted = await postComment(skillsIssueNum, body, github, context);
if (commentPosted) {
console.log(`Success! Entry posted to Skills Issue #${skillsIssueNum}`);
}
}

// If eventActor is team member, open issue and move to "In progress". Else, close issue
const isActiveMember = await checkTeamMembership(github, context, eventActor, TEAM);
let skillsIssueState = "closed";
// Only proceed if Skills Issue message does not include: 'closed', 'assigned', or isArchived
if (!(message.includes('closed') || message.includes('assigned') || isArchived)) {

// If eventActor is team member, open issue and move to "In progress"
const isActiveMember = await checkTeamMembership(github, context, eventActor, TEAM);

if (isActiveMember) {
skillsIssueState = "open";
// Update item's status to "In progress (actively working)" if not already
if (skillsIssueNodeId && skillsStatusId !== IN_PROGRESS_ID) {
await mutateIssueStatus(github, context, skillsIssueNodeId, IN_PROGRESS_ID);
if (isActiveMember) {
try {
await github.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
owner,
repo,
issue_number: skillsIssueNum,
state: "open",
});
console.log(` ⮡ Re-opened issue #${skillsIssueNum}`);
// After delay, update item's status to "In progress (actively working)" if not already
if (skillsIssueNodeId && skillsStatusId !== IN_PROGRESS_ID) {
const statusMutated = await mutateIssueStatus(github, context, skillsIssueNodeId, IN_PROGRESS_ID);
if (statusMutated) console.log(` ⮡ Changed issue #${skillsIssueNum} to "In progress"`);
}
} catch (err) {
console.error(` ⮡ Failed to update issue #${skillsIssueNum} state:`, err);
}
}
}
try {
await github.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
owner,
repo,
issue_number: skillsIssueNum,
state: skillsIssueState,
});
} catch (err) {
console.error(`Failed to update issue #${skillsIssueNum} state:`, err);
}

}

module.exports = postToSkillsIssue;
module.exports = postToSkillsIssue;
3 changes: 2 additions & 1 deletion github-actions/utils/mutate-issue-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ async function mutateIssueStatus(

try {
await github.graphql(mutation, variables);
return true;
} catch (error) {
throw new Error('Error in mutateIssueStatus() function: ' + error);
throw new Error('Error in mutateIssueStatus() function: ' + error);
}
}

Expand Down
1 change: 1 addition & 0 deletions github-actions/utils/post-issue-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async function postComment(issueNum, comment, github, context) {
issue_number: issueNum,
body: comment,
});
return true;
} catch (err) {
throw new Error(err);
}
Expand Down
6 changes: 4 additions & 2 deletions github-actions/utils/query-skills-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ async function querySkillsIssue(github, context, assignee, label) {
id
title
}
isArchived
fieldValues(first: 15) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
Expand Down Expand Up @@ -55,14 +56,15 @@ async function querySkillsIssue(github, context, assignee, label) {
const issueNode = response.repository.issues.nodes[0];

const issueNum = issueNode.number;
const issueId = issueNode.projectItems.nodes[0]?.id;
const issueId = issueNode.projectItems.nodes[0]?.id;
const isArchived = issueNode.projectItems.nodes[0]?.isArchived || false;

const fieldValues = response.repository.issues.nodes[0].projectItems.nodes[0].fieldValues?.nodes ?? [];
const statusField = fieldValues.find(node => node.name && node.optionId);
const statusName = statusField?.name;
const statusId = statusField?.optionId;

return { issueNum, issueId, statusName, statusId };
return { issueNum, issueId, statusName, statusId, isArchived };
} catch (error) {
// If an error occurs, log it and return an object with null values
console.error(`Error querying skills issue: ${error.message}`);
Expand Down