-
-
Notifications
You must be signed in to change notification settings - Fork 844
Create new GHA to add comments to Skills Issue in re: event activities #8248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
t-will-gillis
merged 28 commits into
hackforla:gh-pages
from
t-will-gillis:gha-add-comments-skills-4820
Aug 31, 2025
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
b2d9079
Create member-activity-trigger.yml
t-will-gillis d108995
Rename member-activity-trigger.yml to activity-trigger.yml
t-will-gillis 9f912c6
Create activity-trigger.js
t-will-gillis 9557aef
Create post-to-skills-issue.js
t-will-gillis 297b97c
Create get-skills-issue.js
t-will-gillis d198314
change 'retrieve' --> 'get'
t-will-gillis f99e604
revise comments
t-will-gillis 01154d1
Declare variables
t-will-gillis 190f7bb
declared variables; team --> TEAM; add semicolon; remove variable dec…
t-will-gillis bbca6e8
Add conditional for GHA to run only if 'hackforla/website'
t-will-gillis 424dcd3
Merge branch 'hackforla:gh-pages' into gha-add-comments-skills-4820
t-will-gillis 047e852
edits to match existing Skills Issue comments
t-will-gillis 1736461
reformat history and add error handling
t-will-gillis e6ffb12
major updates to get graphQL working
t-will-gillis e13dcd3
revert to previous
t-will-gillis 2e72548
Merge branch 'hackforla:gh-pages' into gha-add-comments-skills-4820
t-will-gillis 704cb2f
change 'pull request' to 'PR'
t-will-gillis 6c83608
major change- 2 actors PR closed
t-will-gillis ad7183a
tweak for pr.closed if actor same for both, return one
t-will-gillis b7b0874
Update activity-trigger.js
t-will-gillis e67917d
Bump `actions/checkout@v4` --> `@v5` (Dependabot)
t-will-gillis ef5987a
add error handling at fieldValues
t-will-gillis 926d2bf
add in error handling, minor changes
t-will-gillis b541df6
more error catching, change to eventActor
t-will-gillis dc14fb4
Create activity-history-post.yml
t-will-gillis 0c608a9
Merge branch 'gh-pages' into gha-add-comments-skills-4820
t-will-gillis 3c3ec75
Delete .github/workflows/activity-history-post.yml
t-will-gillis 19d31ce
Update check-team-membership.js
t-will-gillis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| name: Member Activity Trigger | ||
|
|
||
| on: | ||
| workflow_call: | ||
| issues: | ||
| types: [opened, assigned, unassigned, closed, reopened] | ||
| issue_comment: | ||
| types: [created] | ||
| pull_request: | ||
| types: [opened, closed, reopened] | ||
| pull_request_review: | ||
| types: [submitted] | ||
| pull_request_review_comment: | ||
| types: [created] | ||
|
|
||
| jobs: | ||
| Gather-Activity-Event-Information: | ||
| runs-on: ubuntu-latest | ||
| if: github.repository == 'hackforla/website' | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: Gather Event Details | ||
| id: gather-event-details | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }} | ||
| script: | | ||
| const script = require('./github-actions/activity-trigger/activity-trigger.js'); | ||
| const activities = script({github, context}); | ||
| return activities; | ||
|
|
||
| - if: ${{ steps.gather-event-details.outputs.result != '[]' }} | ||
| name: Post to Skills Issue | ||
| id: post-to-skills-issue | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }} | ||
| script: | | ||
| const activities = JSON.parse(${{ steps.gather-event-details.outputs.result }}); | ||
| const script = require('./github-actions/activity-trigger/post-to-skills-issue.js'); | ||
| for (const activity of activities) { | ||
| await script({github, context}, activity); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| /** | ||
| * This function parses the triggered event to determine the trigger eventName and eventAction | ||
| * and from this information decide the eventActor (user who is credited for the event). | ||
| * @param {Object} github - GitHub object from function calling activity-trigger.js | ||
| * @param {Object} context - Context of the function calling activity-trigger.js | ||
| * @returns {Object} - An object containing the eventActor and a message | ||
| */ | ||
| 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 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']; | ||
|
|
||
| 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; | ||
| } | ||
| if (eventAction === 'closed') { | ||
| let reason = context.payload.issue.state_reason; | ||
| eventActor = context.payload.issue.user.login; | ||
| eventAction = 'Closed-' + reason; | ||
| } | ||
| } else if (eventName === 'issue_comment') { | ||
| // Check if the comment is on an issue or a pull request | ||
| let isPullRequest = context.payload.issue?.pull_request; | ||
| if (isPullRequest) { | ||
| eventName = 'pull_request_comment'; | ||
| } | ||
| issueNum = context.payload.issue.number; | ||
| eventUrl = context.payload.comment.html_url; | ||
| timeline = context.payload.comment.updated_at; | ||
| } else if (eventName === 'pull_request') { | ||
| issueNum = context.payload.pull_request.number; | ||
| eventUrl = context.payload.pull_request.html_url; | ||
| timeline = context.payload.pull_request.updated_at; | ||
| // If PR closed, check if 'merged' and save 'eventActor' & 'eventPRAuthor' | ||
| if (eventAction === 'closed') { | ||
| eventAction = context.payload.pull_request.merged ? 'PRmerged' : 'PRclosed'; | ||
| eventActor = context.actor; | ||
| eventPRAuthor = context.payload.pull_request.user.login; | ||
| } | ||
| } else if (eventName === 'pull_request_review') { | ||
| issueNum = context.payload.pull_request.number; | ||
| eventUrl = context.payload.review.html_url; | ||
| timeline = context.payload.review.updated_at; | ||
| } | ||
|
|
||
| // Return immediately if the issueNum is a Skills Issue- to discourage | ||
| // infinite loop (recording comment, recording the recording of comment, etc.) | ||
| const isSkillsIssue = await checkIfSkillsIssue(issueNum); | ||
| if (isSkillsIssue) { | ||
| console.log(`- issueNum: ${issueNum} identified as Skills Issue`); | ||
| // return activities; <-- confirm before uncommenting | ||
| } | ||
|
|
||
| // Message templates to post on Skills Issue | ||
| const actionMap = { | ||
| 'issues.opened': 'opened', | ||
| 'issues.Closed-completed': 'closed as completed', | ||
| 'issues.Closed-not_planned': 'closed as not planned', | ||
| 'issues.Closed-duplicate': 'closed as duplicate', | ||
| 'issues.reopened': 'reopened', | ||
| 'issues.assigned': 'assigned', | ||
| 'issues.unassigned': 'unassigned', | ||
| 'issue_comment.created': 'commented', | ||
| 'pull_request_review.created': 'submitted review', | ||
| 'pull_request_comment.created': 'commented', | ||
| 'pull_request.opened': 'opened', | ||
| 'pull_request.PRclosed': 'closed', | ||
| 'pull_request.PRmerged': 'merged', | ||
| 'pull_request.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]); | ||
| } | ||
| // 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]); | ||
| } | ||
| } | ||
|
|
||
| return JSON.stringify(activities); | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Helper function to check if issueNum (that triggered the event) is a Skills Issue | ||
| * @param {Number} issueNum - issueNum to check | ||
| * @returns {Boolean} - true if Skills Issue, false if not | ||
| */ | ||
| async function checkIfSkillsIssue(issueNum) { | ||
| // https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#list-labels-for-an-issue | ||
| const labelData = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNum | ||
| }); | ||
| const isSkillsIssue = labelData.data.some(label => label.name === "Complexity: Prework"); | ||
| return isSkillsIssue; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Helper function to get the date and time in a readable format | ||
| * @param {String} timeline - the date and time string from the event | ||
| * @returns {String} dateTime - formatted date and time string | ||
| */ | ||
| function getDateTime(timeline) { | ||
| const date = new Date(timeline); | ||
| const options = { timeZone: 'America/Los_Angeles', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: true, timeZoneName: 'short' }; | ||
| return date.toLocaleString('en-US', options); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| module.exports = activityTrigger; |
115 changes: 115 additions & 0 deletions
115
github-actions/activity-trigger/post-to-skills-issue.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // Import modules | ||
| const retrieveLabelDirectory = require('../utils/retrieve-label-directory'); | ||
| const querySkillsIssue = require('../utils/query-skills-issue'); | ||
| const postComment = require('../utils/post-issue-comment'); | ||
| const checkTeamMembership = require('../utils/check-team-membership'); | ||
| const statusFieldIds = require('../utils/_data/status-field-ids'); | ||
| const mutateIssueStatus = require('../utils/mutate-issue-status'); | ||
|
|
||
| // `complexity0` refers `Complexity: Prework` label | ||
| const SKILLS_LABEL = retrieveLabelDirectory("complexity0"); | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Function to get eventActor's Skills Issue and post message | ||
| * @param {Object} github - GitHub object | ||
| * @param {Object} context - Context object | ||
| * @param {Object} activity - eventActor and message | ||
| * | ||
| */ | ||
| async function postToSkillsIssue({github, context}, activity) { | ||
|
|
||
| const owner = context.repo.owner; | ||
| const repo = context.repo.repo; | ||
| const TEAM = 'website-write'; | ||
|
|
||
| const [eventActor, message] = activity; | ||
| const MARKER = '<!-- Skills Issue Activity Record -->'; | ||
| const IN_PROGRESS_ID = statusFieldIds('In_Progress'); | ||
|
|
||
| // If eventActor undefined, exit | ||
| if (!eventActor) { | ||
| 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 skillsIssueNum = skillsInfo.issueNum; | ||
| const skillsIssueNodeId = skillsInfo.issueId; | ||
| const skillsStatusId = skillsInfo.statusId; | ||
|
|
||
| // Return immediately if Skills Issue not found | ||
| if (skillsIssueNum) { | ||
| console.log(`Found Skills Issue for ${eventActor}: #${skillsIssueNum}`); | ||
| } else { | ||
| console.log(`Did not find Skills Issue for ${eventActor}. Cannot post message.`); | ||
| return; | ||
| } | ||
|
|
||
| // Get all comments from the Skills Issue | ||
| let commentData; | ||
| try { | ||
| // https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#list-issue-comments | ||
| commentData = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/comments', { | ||
| owner, | ||
| repo, | ||
| issue_number: skillsIssueNum, | ||
| }); | ||
| } catch (err) { | ||
| console.error(`GET comments failed for issue #${skillsIssueNum}:`, err); | ||
| return; | ||
| } | ||
|
|
||
| // Find the comment that includes the MARKER text and append message | ||
| const commentFound = commentData.data.find(comment => comment.body.includes(MARKER)); | ||
| const commentFoundId = commentFound ? commentFound.id : null; | ||
|
|
||
| if (commentFound) { | ||
| console.log(`Found comment with MARKER: ${MARKER}`); | ||
| const commentId = commentFoundId; | ||
| const originalBody = commentFound.body; | ||
| const updatedBody = `${originalBody}\n${message}`; | ||
| try { | ||
| // https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#update-an-issue-comment | ||
| await github.request('PATCH /repos/{owner}/{repo}/issues/comments/{commentId}', { | ||
| owner, | ||
| repo, | ||
| commentId, | ||
| body: updatedBody | ||
| }); | ||
| } catch (err) { | ||
| console.error(`Something went wrong updating comment:`, err); | ||
| } | ||
|
|
||
| } else { | ||
| console.log(`MARKER not found in comments, creating new comment 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); | ||
| } | ||
|
|
||
| // 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"; | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| 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) | ||
Check noticeCode scanning / CodeQL Semicolon insertion Note
Avoid automated semicolon insertion (97% of all statements in
the enclosing function Error loading related location Loading |
||
| } | ||
| } | ||
|
|
||
| module.exports = postToSkillsIssue; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.