Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 Jul 22, 2025
d108995
Rename member-activity-trigger.yml to activity-trigger.yml
t-will-gillis Jul 22, 2025
9f912c6
Create activity-trigger.js
t-will-gillis Jul 22, 2025
9557aef
Create post-to-skills-issue.js
t-will-gillis Jul 22, 2025
297b97c
Create get-skills-issue.js
t-will-gillis Jul 22, 2025
d198314
change 'retrieve' --> 'get'
t-will-gillis Jul 22, 2025
f99e604
revise comments
t-will-gillis Jul 22, 2025
01154d1
Declare variables
t-will-gillis Jul 28, 2025
190f7bb
declared variables; team --> TEAM; add semicolon; remove variable dec…
t-will-gillis Jul 28, 2025
bbca6e8
Add conditional for GHA to run only if 'hackforla/website'
t-will-gillis Jul 28, 2025
424dcd3
Merge branch 'hackforla:gh-pages' into gha-add-comments-skills-4820
t-will-gillis Aug 11, 2025
047e852
edits to match existing Skills Issue comments
t-will-gillis Aug 11, 2025
1736461
reformat history and add error handling
t-will-gillis Aug 13, 2025
e6ffb12
major updates to get graphQL working
t-will-gillis Aug 14, 2025
e13dcd3
revert to previous
t-will-gillis Aug 15, 2025
2e72548
Merge branch 'hackforla:gh-pages' into gha-add-comments-skills-4820
t-will-gillis Aug 15, 2025
704cb2f
change 'pull request' to 'PR'
t-will-gillis Aug 17, 2025
6c83608
major change- 2 actors PR closed
t-will-gillis Aug 19, 2025
ad7183a
tweak for pr.closed if actor same for both, return one
t-will-gillis Aug 19, 2025
b7b0874
Update activity-trigger.js
t-will-gillis Aug 20, 2025
e67917d
Bump `actions/checkout@v4` --> `@v5` (Dependabot)
t-will-gillis Aug 24, 2025
ef5987a
add error handling at fieldValues
t-will-gillis Aug 25, 2025
926d2bf
add in error handling, minor changes
t-will-gillis Aug 27, 2025
b541df6
more error catching, change to eventActor
t-will-gillis Aug 27, 2025
dc14fb4
Create activity-history-post.yml
t-will-gillis Aug 31, 2025
0c608a9
Merge branch 'gh-pages' into gha-add-comments-skills-4820
t-will-gillis Aug 31, 2025
3c3ec75
Delete .github/workflows/activity-history-post.yml
t-will-gillis Aug 31, 2025
19d31ce
Update check-team-membership.js
t-will-gillis Aug 31, 2025
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
40 changes: 40 additions & 0 deletions .github/workflows/activity-trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Member Activity Trigger

on:
workflow_call:
issues:
types: [opened, assigned, unassigned, closed]
issue_comment:
types: [created]
pull_request:
types: [opened, closed]
pull_request_review:
types: [submitted]

jobs:
Gather-Activity-Event-Information:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- 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 activity = script({g: github, c: context})
return activity

- 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 activity = ${{ steps.gather-event-details.outputs.result }}
const script = require('./github-actions/activity-trigger/post-to-skills-issue.js')
script({g: github, c: context}, activity)

123 changes: 123 additions & 0 deletions github-actions/activity-trigger/activity-trigger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* This function is triggered by member activities, which include:
* the eventName (i.e. "issues", "pull_request", "pull_request_review", etc. ),
* the eventAction (i.e. "opened", "assigned", "submitted", etc.), and
* 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({g, c}) {

github = g;
context = c;

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

let eventName = context.eventName;
let eventAction = context.payload.action;
let eventActor = context.actor;
let activity = [];

const excludedActors = ['HackforLABot', 'elizabethhonest'];

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;
eventAction = reason;
}
} else if (eventName === 'issue_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 change eventActor to the original pr author
if (eventAction === 'closed') {
eventAction = context.payload.pull_request.merged ? 'merged' : 'closed';
eventActor = 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;
}

// Following are for confirmation, can be removed
console.log(`eventName = ${eventName}`);
console.log(`eventAction = ${eventAction}`);
console.log(`eventActor = ${eventActor}`);
console.log(`issueNum = ${issueNum}`);
console.log(`eventUrl = ${eventUrl}`);
console.log(`eventTime = ${timeline}`);

// 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 activity;
}
// Return immediately if the eventActor is a bot- same reason
if (eventActor in excludedActors) {
return activity;
}

// Message templates to post on Skills Issue
const actionMap = {
'issues.opened': 'opened an issue',
'issues.completed': 'closed an issue as completed',
'issues.not_planned': 'closed an issue as not planned',
'issues.duplicate': 'closed an issue as duplicate',
'issues.assigned': 'been assigned to an issue',
'issues.unassigned': 'been unassigned from an issue',
'issue_comment.created': 'commented on an issue or pr',
'pull_request.opened': 'opened a pull request',
'pull_request.closed': 'had a pull request closed w/o merging',
'pull_request.merged': 'had a pull request merged',
'pull_request_review.submitted': 'submitted a pull request review'
};
const action = actionMap[`${eventName}.${eventAction}`];
let message = `@ ${eventActor} has ${action}: #[${issueNum}](${eventUrl}) at ${timeline}`;
console.log(message);

activity = [eventActor, message];
return activity;



/**
* Helper function to check if issueNum references a Skills Issue
* @param {Number} issueNum - issue number 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;
}
}

module.exports = activityTrigger;
85 changes: 85 additions & 0 deletions github-actions/activity-trigger/post-to-skills-issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Import modules
const retrieveSkillsIssue = require('../utils/retrieve-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');



/**
* Function to retrieve Skills Issue and post message
* @param {Object} github - GitHub object
* @param {Object} context - Context object
* @param {Object} activity - eventActor and message
*
*/
async function postToSkillsIssue({g, c}, activity) {

github = g;
context = c;

const owner = context.repo.owner;
const repo = context.repo.repo;
const TEAM = 'website-write';

const username = activity[0];
const message = activity[1];
const MARKER = '<!-- Skills Issue Activity Record -->';

// Retrieve user's Skills Issue
const { skillsIssueNum, skillsIssueNodeId } = await retrieveSkillsIssue(username);
// Return immediately if Skills Issue not found
if (skillsIssueNum) {
console.log(`Found Skills Issue for ${username}: ${skillsIssueNum}`);
} else {
console.log(`Did not find Skills Issue for ${username}. Cannot post message.`);
return;
}

// Retrieve all comments from the Skills Issue
// https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#list-issue-comments
const commentData = await github.request('GET /repos/{owner}/{repo}/issues/{issueNum}/comments', {
owner,
repo,
issueNum: skillsIssueNum,
});

// 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) {
const commentId = commentFoundId;
const originalBody = commentFound.body;
const updatedBody = `${originalBody}\n${message}`;
// https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#update-an-issue-comment
const patchSkillsIssue = await github.request('PATCH /repos/{owner}/{repo}/issues/comments/{commentId}', {
owner,
repo,
commentId,
body: updatedBody
});
} else {
const body = `${MARKER}\n## Activity Log: ${username}\n${message}`;
await postComment(github, context, skillsIssueNum, body);
}

// Check whether eventActor is team member; if so open issue and move to "In progress"
const isActiveMember = await checkTeamMembership(github, username, team);

if (isActiveMember) {
// Make sure Skills Issue is open
await github.request('PATCH /repos/{owner}/{repo}/issues/{issueNum}', {
owner,
repo,
issueNum: skillsIssueNum,
state: "open",
});
// Update item's status to "In progress (actively working)"
let statusValue = statusFieldIds('In_Progress');
await mutateIssueStatus(github, context, skillsIssueNodeId, statusValue);
}
}

module.exports = postToSkillsIssue;
29 changes: 29 additions & 0 deletions github-actions/utils/get-skills-issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Helper function to retrieve eventActor's Skills Issue
* @param {String} eventActor - Key reference to look up user's Skill Issue
* @return {Object} - eventActor's skillsIssueNum and skillsIssueNodeId
*/
async function retrieveSkillsIssue(eventActor) {
// Note that this returns the first 10 issues assigned to the eventActor- which is presumed
// to include that person's Skills Issue- this might need to be changed if Skill's Issues not found
// https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues
const issueData = await github.request('GET /repos/{owner}/{repo}/issues', {
owner: context.repo.owner,
repo: context.repo.repo,
assignee: eventActor,
state: 'all',
direction: 'asc',
per_page: 10,
});

// Find issue with the `Complexity: Prework` label, then extract skillsIssueNum and skillsIssueNodeId
const skillsIssue = issueData.data.find(issue => issue.labels.some(label => label.name === "Complexity: Prework"));
const skillsIssueNum = skillsIssue ? skillsIssue.number : null;
const skillsIssueNodeId = skillsIssue ? skillsIssue.node_id : null;

console.log(`Found skills issue ${skillsIssueNum}: ${skillsIssueNodeId}`);

return {skillsIssueNum, skillsIssueNodeId};
}

module.exports = retrieveSkillsIssue;