Skip to content

Commit 8998f3a

Browse files
Create new GHA to add comments to Skills Issue in re: event activities (#8248)
* Create member-activity-trigger.yml initial commit of yml * Rename member-activity-trigger.yml to activity-trigger.yml * Create activity-trigger.js * Create post-to-skills-issue.js * Create get-skills-issue.js * change 'retrieve' --> 'get' * revise comments * Declare variables * declared variables; team --> TEAM; add semicolon; remove variable declaration * Add conditional for GHA to run only if 'hackforla/website' * edits to match existing Skills Issue comments * reformat history and add error handling * major updates to get graphQL working * revert to previous * change 'pull request' to 'PR' * major change- 2 actors PR closed * tweak for pr.closed if actor same for both, return one * Update activity-trigger.js For issue closed, record both closer and the issue assignee with event * Bump `actions/checkout@v4` --> `@v5` (Dependabot) * add error handling at fieldValues * add in error handling, minor changes * more error catching, change to eventActor * Create activity-history-post.yml add back changed file, then re-delete * Delete .github/workflows/activity-history-post.yml re-delete (fixing merge conflict) * Update check-team-membership.js fixed ===
1 parent 501b6f7 commit 8998f3a

File tree

8 files changed

+407
-46
lines changed

8 files changed

+407
-46
lines changed

.github/workflows/activity-history-post.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Member Activity Trigger
2+
3+
on:
4+
workflow_call:
5+
issues:
6+
types: [opened, assigned, unassigned, closed, reopened]
7+
issue_comment:
8+
types: [created]
9+
pull_request:
10+
types: [opened, closed, reopened]
11+
pull_request_review:
12+
types: [submitted]
13+
pull_request_review_comment:
14+
types: [created]
15+
16+
jobs:
17+
Gather-Activity-Event-Information:
18+
runs-on: ubuntu-latest
19+
if: github.repository == 'hackforla/website'
20+
steps:
21+
- uses: actions/checkout@v5
22+
23+
- name: Gather Event Details
24+
id: gather-event-details
25+
uses: actions/github-script@v7
26+
with:
27+
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }}
28+
script: |
29+
const script = require('./github-actions/activity-trigger/activity-trigger.js');
30+
const activities = script({github, context});
31+
return activities;
32+
33+
- if: ${{ steps.gather-event-details.outputs.result != '[]' }}
34+
name: Post to Skills Issue
35+
id: post-to-skills-issue
36+
uses: actions/github-script@v7
37+
with:
38+
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }}
39+
script: |
40+
const activities = JSON.parse(${{ steps.gather-event-details.outputs.result }});
41+
const script = require('./github-actions/activity-trigger/post-to-skills-issue.js');
42+
for (const activity of activities) {
43+
await script({github, context}, activity);
44+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* This function parses the triggered event to determine the trigger eventName and eventAction
3+
* and from this information decide the eventActor (user who is credited for the event).
4+
* @param {Object} github - GitHub object from function calling activity-trigger.js
5+
* @param {Object} context - Context of the function calling activity-trigger.js
6+
* @returns {Object} - An object containing the eventActor and a message
7+
*/
8+
async function activityTrigger({github, context}) {
9+
10+
let issueNum = '';
11+
let assignee = '';
12+
let timeline = '';
13+
14+
let eventName = context.eventName;
15+
let eventAction = context.payload.action;
16+
let eventActor = context.actor;
17+
let eventPRAuthor = '';
18+
let activities = [];
19+
20+
// Exclude all bot actors from being recorded as a guardrail against infinite loops
21+
const EXCLUDED_ACTORS = ['HackforLABot', 'elizabethhonest', 'github-actions', 'github-advanced-security', 'github-pages', 'dependabot[bot]', 'dependabot-preview[bot]', 'dependabot', 'dependabot-preview'];
22+
23+
if (eventName === 'issues') {
24+
issueNum = context.payload.issue.number;
25+
eventUrl = context.payload.issue.html_url;
26+
timeline = context.payload.issue.updated_at;
27+
// If issue action is not opened and an assignee exists, then change
28+
// the eventActor to the issue assignee, else retain issue author
29+
assignee = context.payload.assignee?.login;
30+
if (eventAction != 'opened' && assignee != null ) {
31+
console.log(`Issue is ${eventAction}. Change eventActor => ${assignee}`);
32+
eventActor = assignee;
33+
} else {
34+
eventActor = context.payload.issue.user.login;
35+
}
36+
if (eventAction === 'closed') {
37+
let reason = context.payload.issue.state_reason;
38+
eventActor = context.payload.issue.user.login;
39+
eventAction = 'Closed-' + reason;
40+
}
41+
} else if (eventName === 'issue_comment') {
42+
// Check if the comment is on an issue or a pull request
43+
let isPullRequest = context.payload.issue?.pull_request;
44+
if (isPullRequest) {
45+
eventName = 'pull_request_comment';
46+
}
47+
issueNum = context.payload.issue.number;
48+
eventUrl = context.payload.comment.html_url;
49+
timeline = context.payload.comment.updated_at;
50+
} else if (eventName === 'pull_request') {
51+
issueNum = context.payload.pull_request.number;
52+
eventUrl = context.payload.pull_request.html_url;
53+
timeline = context.payload.pull_request.updated_at;
54+
// If PR closed, check if 'merged' and save 'eventActor' & 'eventPRAuthor'
55+
if (eventAction === 'closed') {
56+
eventAction = context.payload.pull_request.merged ? 'PRmerged' : 'PRclosed';
57+
eventActor = context.actor;
58+
eventPRAuthor = context.payload.pull_request.user.login;
59+
}
60+
} else if (eventName === 'pull_request_review') {
61+
issueNum = context.payload.pull_request.number;
62+
eventUrl = context.payload.review.html_url;
63+
timeline = context.payload.review.updated_at;
64+
}
65+
66+
// Return immediately if the issueNum is a Skills Issue- to discourage
67+
// infinite loop (recording comment, recording the recording of comment, etc.)
68+
const isSkillsIssue = await checkIfSkillsIssue(issueNum);
69+
if (isSkillsIssue) {
70+
console.log(`- issueNum: ${issueNum} identified as Skills Issue`);
71+
// return activities; <-- confirm before uncommenting
72+
}
73+
74+
// Message templates to post on Skills Issue
75+
const actionMap = {
76+
'issues.opened': 'opened',
77+
'issues.Closed-completed': 'closed as completed',
78+
'issues.Closed-not_planned': 'closed as not planned',
79+
'issues.Closed-duplicate': 'closed as duplicate',
80+
'issues.reopened': 'reopened',
81+
'issues.assigned': 'assigned',
82+
'issues.unassigned': 'unassigned',
83+
'issue_comment.created': 'commented',
84+
'pull_request_review.created': 'submitted review',
85+
'pull_request_comment.created': 'commented',
86+
'pull_request.opened': 'opened',
87+
'pull_request.PRclosed': 'closed',
88+
'pull_request.PRmerged': 'merged',
89+
'pull_request.reopened': 'reopened'
90+
};
91+
92+
let localTime = getDateTime(timeline);
93+
let action = actionMap[`${eventName}.${eventAction}`];
94+
let message = `- ${eventActor} ${action}: ${eventUrl} at ${localTime}`;
95+
96+
// Check to confirm the eventActor isn't a bot
97+
const isExcluded = (eventActor) => EXCLUDED_ACTORS.includes(eventActor);
98+
if (!isExcluded(eventActor)) {
99+
console.log(`Not a bot. Message to post: ${message}`);
100+
activities.push([eventActor, message]);
101+
}
102+
103+
// Only if issue is closed, and eventActor != assignee, return assignee and message
104+
if (eventAction.includes('Closed-') && (eventActor !== assignee)) {
105+
message = `- ${assignee} issue ${action}: ${eventUrl} at ${localTime}`;
106+
activities.push([assignee, message]);
107+
}
108+
// Only if PRclosed or PRmerged, and PRAuthor != eventActor, return PRAuthor and message
109+
if ((eventAction === 'PRclosed' || eventAction === 'PRmerged') && (eventActor != eventPRAuthor)) {
110+
let messagePRAuthor = `- ${eventPRAuthor} PR was ${action}: ${eventUrl} at ${localTime}`;
111+
if (!isExcluded(eventPRAuthor)) {
112+
console.log(`Not a bot. Message to post: ${messagePRAuthor}`);
113+
activities.push([eventPRAuthor, messagePRAuthor]);
114+
}
115+
}
116+
117+
return JSON.stringify(activities);
118+
119+
120+
121+
/**
122+
* Helper function to check if issueNum (that triggered the event) is a Skills Issue
123+
* @param {Number} issueNum - issueNum to check
124+
* @returns {Boolean} - true if Skills Issue, false if not
125+
*/
126+
async function checkIfSkillsIssue(issueNum) {
127+
// https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#list-labels-for-an-issue
128+
const labelData = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', {
129+
owner: context.repo.owner,
130+
repo: context.repo.repo,
131+
issue_number: issueNum
132+
});
133+
const isSkillsIssue = labelData.data.some(label => label.name === "Complexity: Prework");
134+
return isSkillsIssue;
135+
}
136+
137+
138+
139+
/**
140+
* Helper function to get the date and time in a readable format
141+
* @param {String} timeline - the date and time string from the event
142+
* @returns {String} dateTime - formatted date and time string
143+
*/
144+
function getDateTime(timeline) {
145+
const date = new Date(timeline);
146+
const options = { timeZone: 'America/Los_Angeles', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: true, timeZoneName: 'short' };
147+
return date.toLocaleString('en-US', options);
148+
}
149+
150+
}
151+
152+
module.exports = activityTrigger;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Import modules
2+
const retrieveLabelDirectory = require('../utils/retrieve-label-directory');
3+
const querySkillsIssue = require('../utils/query-skills-issue');
4+
const postComment = require('../utils/post-issue-comment');
5+
const checkTeamMembership = require('../utils/check-team-membership');
6+
const statusFieldIds = require('../utils/_data/status-field-ids');
7+
const mutateIssueStatus = require('../utils/mutate-issue-status');
8+
9+
// `complexity0` refers `Complexity: Prework` label
10+
const SKILLS_LABEL = retrieveLabelDirectory("complexity0");
11+
12+
13+
14+
/**
15+
* Function to get eventActor's Skills Issue and post message
16+
* @param {Object} github - GitHub object
17+
* @param {Object} context - Context object
18+
* @param {Object} activity - eventActor and message
19+
*
20+
*/
21+
async function postToSkillsIssue({github, context}, activity) {
22+
23+
const owner = context.repo.owner;
24+
const repo = context.repo.repo;
25+
const TEAM = 'website-write';
26+
27+
const [eventActor, message] = activity;
28+
const MARKER = '<!-- Skills Issue Activity Record -->';
29+
const IN_PROGRESS_ID = statusFieldIds('In_Progress');
30+
31+
// If eventActor undefined, exit
32+
if (!eventActor) {
33+
console.log(`eventActor is undefined (likely a bot). Cannot post message.`);
34+
return;
35+
}
36+
37+
// Get eventActor's Skills Issue number, nodeId, current statusId (all null if no Skills Issue found)
38+
const skillsInfo = await querySkillsIssue(github, context, eventActor, SKILLS_LABEL);
39+
const skillsIssueNum = skillsInfo.issueNum;
40+
const skillsIssueNodeId = skillsInfo.issueId;
41+
const skillsStatusId = skillsInfo.statusId;
42+
43+
// Return immediately if Skills Issue not found
44+
if (skillsIssueNum) {
45+
console.log(`Found Skills Issue for ${eventActor}: #${skillsIssueNum}`);
46+
} else {
47+
console.log(`Did not find Skills Issue for ${eventActor}. Cannot post message.`);
48+
return;
49+
}
50+
51+
// Get all comments from the Skills Issue
52+
let commentData;
53+
try {
54+
// https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#list-issue-comments
55+
commentData = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/comments', {
56+
owner,
57+
repo,
58+
issue_number: skillsIssueNum,
59+
});
60+
} catch (err) {
61+
console.error(`GET comments failed for issue #${skillsIssueNum}:`, err);
62+
return;
63+
}
64+
65+
// Find the comment that includes the MARKER text and append message
66+
const commentFound = commentData.data.find(comment => comment.body.includes(MARKER));
67+
const commentFoundId = commentFound ? commentFound.id : null;
68+
69+
if (commentFound) {
70+
console.log(`Found comment with MARKER: ${MARKER}`);
71+
const commentId = commentFoundId;
72+
const originalBody = commentFound.body;
73+
const updatedBody = `${originalBody}\n${message}`;
74+
try {
75+
// https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#update-an-issue-comment
76+
await github.request('PATCH /repos/{owner}/{repo}/issues/comments/{commentId}', {
77+
owner,
78+
repo,
79+
commentId,
80+
body: updatedBody
81+
});
82+
} catch (err) {
83+
console.error(`Something went wrong updating comment:`, err);
84+
}
85+
86+
} else {
87+
console.log(`MARKER not found in comments, creating new comment with MARKER...`);
88+
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}`;
89+
await postComment(skillsIssueNum, body, github, context);
90+
}
91+
92+
// If eventActor is team member, open issue and move to "In progress". Else, close issue
93+
const isActiveMember = await checkTeamMembership(github, context, eventActor, TEAM);
94+
let skillsIssueState = "closed";
95+
96+
if (isActiveMember) {
97+
skillsIssueState = "open";
98+
// Update item's status to "In progress (actively working)" if not already
99+
if (skillsIssueNodeId && skillsStatusId !== IN_PROGRESS_ID) {
100+
await mutateIssueStatus(github, context, skillsIssueNodeId, IN_PROGRESS_ID);
101+
}
102+
}
103+
try {
104+
await github.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
105+
owner,
106+
repo,
107+
issue_number: skillsIssueNum,
108+
state: skillsIssueState,
109+
});
110+
} catch (err) {
111+
console.error(`Failed to update issue #${skillsIssueNum} state:`, err)
112+
}
113+
}
114+
115+
module.exports = postToSkillsIssue;

github-actions/trigger-schedule/add-update-label-weekly/add-label.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ const [
1919
er,
2020
epic,
2121
dependency,
22+
skillsIssueCompleted,
2223
] = [
2324
"statusUpdated",
2425
"statusInactive1",
2526
"statusInactive2",
2627
"draft",
2728
"er",
2829
"epic",
29-
"dependency"
30+
"dependency",
31+
"skillsIssueCompleted",
3032
].map(retrieveLabelDirectory);
3133

3234
const updatedByDays = 3; // If last update update 3 days, the issue is considered updated
@@ -100,7 +102,7 @@ async function main({ g, c }) {
100102
* @returns {Promise<Array>} issueNums - an array of open, assigned, and statused issue numbers
101103
*/
102104
async function getIssueNumsFromRepo() {
103-
const labelsToExclude = [draft, er, epic, dependency];
105+
const labelsToExclude = [draft, er, epic, dependency, skillsIssueCompleted];
104106
let issueNums = [];
105107
let pageNum = 1;
106108
let result = [];
@@ -369,7 +371,7 @@ function formatComment(assignees, labelString) {
369371
function isCommentByBot(data) {
370372
let botLogin = "github-actions[bot]";
371373
let hflaBotLogin = "HackforLABot";
372-
// If the comment includes the MARKER, return false
374+
// If the comment includes the MARKER, return false so it is not minimized
373375
let MARKER = '<!-- Skills Issue Activity Record -->';
374376
if (data.body.includes(MARKER)) {
375377
console.log(`Found "Skills Issue Activity Record" - do not minimize`);

0 commit comments

Comments
 (0)