Skip to content

Commit

Permalink
Consolidate functions in schedule-monthly.yml 5969 (#6011)
Browse files Browse the repository at this point in the history
* Create inactive-members.md

* Update inactive-members.md

* Update create-new-issue.js

* Create issue-template-parser.js

* Update issue-template-parser.js

* Update inactive-members.md

* Update create-new-issue.js

* Update inactive-members.md

* Update issue-template-parser.js

* Update contributors-data.js

up to line 145

* Update contributors-data.js

up to line 207

* Update contributors-data.js

* Update create-new-issue.js

* Update contributors-data.js

* Update contributors-data.js

Added semicolon per CodeQL no. 85

* Update check-labels.js

* Update contributors-data.js

Rename functions:
- getTimeline --> getEventTimeline
- isTimelineOutdated --> isEventOutdated
To differentiate from sim functs in `add-label.js`

* Update check-labels.js

Edits to resolve merge conflict

* Update contributors-data.js

rename `toRemove` --> `shouldRemoveOrNotify`

* Update contributors-data.js

clarification of comment

* Update contributors-data.js

incorporating comments from 1/5/24 dev meeting

* Update contributors-data.js

First pass addressing PR review comments

* Update schedule-monthly.yml

`comment-issue` step replaced by refactoring

* Update create-new-issue.js

Refactored to replace `comment-issue.js` with `post-issue-comment.js`

* Delete github-actions/trigger-schedule/list-inactive-members/comment-issue.js

refactored `comment-issue.js` with `/util/post-issue-comment.js`

* Update create-new-issue.js

adding ";" per CodeQL recommendation

* Update contributors-data.js

Remove lastCommentTimestamp checks in isEventOutdated()

* Update contributors-data.js

* Update schedule-monthly.yml

Remove unnecessary variable on line 77

* Update contributors-data.js
  • Loading branch information
t-will-gillis authored Jan 25, 2024
1 parent 6b06f0c commit 3a2f3d9
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 157 deletions.
18 changes: 3 additions & 15 deletions .github/workflows/schedule-monthly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,7 @@ jobs:
with:
github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }}
script: |
const artifactContent = process.env.TRIM_LISTS;
const script = require('./github-actions/trigger-schedule/list-inactive-members/create-new-issue.js');
const createNewIssue = script({g: github, c: context}, artifactContent);
return createNewIssue;
const artifactContent = process.env.TRIM_LISTS
const script = require('./github-actions/trigger-schedule/list-inactive-members/create-new-issue.js')
script({g: github, c: context}, artifactContent)
# Comments on issue #2607, notifying leads that the above issue has been created
- name: Comment issue
uses: actions/github-script@v7
id: comment-issue
with:
github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }}
script: |
const script = require('./github-actions/trigger-schedule/list-inactive-members/comment-issue.js');
const newIssueNumber = ${{ steps.create-new-issue.outputs.result }};
script({g: github, c: context}, newIssueNumber);
201 changes: 142 additions & 59 deletions github-actions/trigger-schedule/github-data/contributors-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ const org = 'hackforla';
const repo = 'website';
const team = 'website-write';
const baseTeam = 'website';
const maintTeam = 'website-maintain';

// Set date limits: we are sorting inactive members into groups to warn after 1 month and remove after 2 months.
// Since the website team takes off the month of December, the January 1st run is skipped (via `schedule-monthly.yml`).
// The February 1st run keeps the 1 month inactive warning, but changes removal to 3 months inactive (skipping December).
let today = new Date();
let oneMonth = (today.getMonth() == 1) ? 2 : 1; // If month is "February" == 1, then oneMonth = 2 months ago
let twoMonths = (today.getMonth() == 1) ? 3 : 2; // If month is "February" == 1, then twoMonths = 3 months ago

let oneMonthAgo = new Date(); // oneMonthAgo instantiated with date of "today"
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); // then set oneMonthAgo from "today"
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - oneMonth); // then set oneMonthAgo from "today"
oneMonthAgo = oneMonthAgo.toISOString();
let twoMonthsAgo = new Date(); // twoMonthsAgo instantiated with date of "today"
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - twoMonths); // then set twoMonthsAgo from "today"
Expand All @@ -30,22 +32,26 @@ twoMonthsAgo = twoMonthsAgo.toISOString();
* Main function, immediately invoked
*/
(async function main(){
const [contributorsOneMonthAgo, contributorsTwoMonthsAgo] = await fetchContributors();
const [contributorsOneMonthAgo, contributorsTwoMonthsAgo, inactiveWithOpenIssue] = await fetchContributors();
console.log('-------------------------------------------------------');
console.log('List of active contributors since' + ' ⏰ ' + oneMonthAgo.slice(0, 10) + ':');
console.log('List of active contributors since ' + oneMonthAgo.slice(0, 10) + ':');
console.log(contributorsOneMonthAgo);

const currentTeamMembers = await fetchTeamMembers();
const currentTeamMembers = await fetchTeamMembers(team);
console.log('-------------------------------------------------------');
console.log('Current members of ' + team + ':')
console.log(currentTeamMembers)

const removedContributors = await removeInactiveMembers(currentTeamMembers, contributorsTwoMonthsAgo);
const [removedContributors, cannotRemoveYet] = await removeInactiveMembers(currentTeamMembers, contributorsTwoMonthsAgo, inactiveWithOpenIssue);
console.log('-------------------------------------------------------');
console.log('Removed members from ' + team + ' inactive since ' + twoMonthsAgo.slice(0, 10) + ':');
console.log(removedContributors);

const updatedTeamMembers = await fetchTeamMembers();
console.log('-------------------------------------------------------');
console.log('Members inactive since ' + twoMonthsAgo.slice(0, 10) + ' with open issues preventing removal:');
console.log(cannotRemoveYet);

const updatedTeamMembers = await fetchTeamMembers(team);
const notifiedContributors = await notifyInactiveMembers(updatedTeamMembers, contributorsOneMonthAgo);
console.log('-------------------------------------------------------');
console.log('Notified members from ' + team + ' inactive since ' + oneMonthAgo.slice(0, 10) + ':');
Expand All @@ -64,7 +70,11 @@ twoMonthsAgo = twoMonthsAgo.toISOString();
async function fetchContributors(){
let allContributorsSinceOneMonthAgo = {};
let allContributorsSinceTwoMonthsAgo = {};
let inactiveWithOpenIssue = {};

// Members on 'website-maintain' team considered permanent members
const permanentMembers = await fetchTeamMembers(maintTeam);

// Fetch all contributors with commit, comment, and issue (assignee) contributions
const APIs = ['GET /repos/{owner}/{repo}/commits', 'GET /repos/{owner}/{repo}/issues/comments', 'GET /repos/{owner}/{repo}/issues'];
const dates = [oneMonthAgo, twoMonthsAgo];
Expand All @@ -84,7 +94,7 @@ async function fetchContributors(){
since: date,
per_page: 100,
page: pageNum
})
});

// If the API call returns an empty array, break out of loop- there is no additional data.
// Else if data is returned, push it to `result` and increase the page number (`pageNum`)
Expand All @@ -104,33 +114,107 @@ async function fetchContributors(){
if(contributorInfo.author){
allContributorsSince[contributorInfo.author.login] = true;
}
// Check for username in `user.login`, but skip `user.login` for 3rd API
// Check for username in `user.login`, but skip `user.login` covered by 3rd API
else if(contributorInfo.user && api != 'GET /repos/{owner}/{repo}/issues'){
allContributorsSince[contributorInfo.user.login] = true;
}
// This check is done for `/issues` (3rd) API. Sometimes a user who created an issue is not the same as the
// assignee on that issue- we want to make sure that we count all assignees as active contributors as well.
else if(contributorInfo.assignee){
allContributorsSince[contributorInfo.assignee.login] = true;
}
}
}
// We only want to run this check if the assignee is not counted as an active contributor yet.
else if((contributorInfo.assignee) && (contributorInfo.assignee.login in allContributorsSince === false)){
const issueNum = contributorInfo.number;
const timeline = await getEventTimeline(issueNum);
const assignee = contributorInfo.assignee.login;
const responseObject = await isEventOutdated(date, timeline, assignee);
// If timeline is not outdated, add member to `allContributorsSince`
if(responseObject.result === false){
allContributorsSince[assignee] = true;
}
// If timeline is more than two months ago, and the issue title does not include
// the words "Pre-work Checklist", add to open issues with inactive comments
else {
if(date == twoMonthsAgo && !contributorInfo.title.includes("Pre-work Checklist")){
inactiveWithOpenIssue[assignee] = issueNum;
}
}
}
}
}
// Add permanent members from 'website-maintain' to list of active contributors
for(const permanentMember in permanentMembers){
allContributorsSince[permanentMember] = true;
}
if(date == oneMonthAgo){
allContributorsSinceOneMonthAgo = allContributorsSince;
} else {
allContributorsSinceTwoMonthsAgo = allContributorsSince;
}
}
return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo];
return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo, inactiveWithOpenIssue];
}


/*
* Helper functions for fetchContributors()
*
*
*/
async function getEventTimeline(issueNum) {
let timelineArray = []
let page = 1
while (true) {
try {
const results = await octokit.rest.issues.listEventsForTimeline({
owner: org,
repo: repo,
issue_number: issueNum,
per_page: 100,
page: page,
});
if (results.data.length) {
timelineArray = timelineArray.concat(results.data);
} else {
break
}
} catch (err) {
console.log(err);
continue
}
finally {
page++
}
}
return timelineArray
}


function isEventOutdated(date, timeline, assignee) {
let lastAssignedTimestamp = null;
for (let i = timeline.length - 1; i >= 0; i--) {
let eventObj = timeline[i];
let eventType = eventObj.event;
let eventTimestamp = eventObj.updated_at || eventObj.created_at;

// update the lastAssignedTimestamp if this is the last (most recent) time an assignee was assigned to the issue
if (!lastAssignedTimestamp && eventType === 'assigned' && assignee === (eventObj.assignee.login)) {
lastAssignedTimestamp = eventTimestamp;
}
}
// If the assignee was assigned later than the 'date', the issue is not outdated so return false
if (lastAssignedTimestamp && (lastAssignedTimestamp >= date)) {
return { result: false };
}
return { result: true };
}



/**
* Function to return list of current team members
* @param {String} team_slug - default to 'website-write' team
* @returns {Array} allMembers - Current team members
*/
async function fetchTeamMembers(){
async function fetchTeamMembers(fetchTeam){

let pageNum = 1;
let teamResults = [];
Expand All @@ -139,7 +223,7 @@ async function fetchTeamMembers(){
while(true){
const teamMembers = await octokit.request('GET /orgs/{org}/teams/{team_slug}/members', {
org: org,
team_slug: team,
team_slug: fetchTeam,
per_page: 100,
page: pageNum
})
Expand All @@ -165,13 +249,14 @@ async function fetchTeamMembers(){
* @param {Object} recentContributors - List of active contributors
* @returns {Array} removed members - List of members that were removed
*/
async function removeInactiveMembers(currentTeamMembers, recentContributors){
async function removeInactiveMembers(currentTeamMembers, recentContributors, inactiveWithOpenIssue){
const removedMembers = [];
const cannotRemoveYet = {};

// Loop over team members and remove them from the team if they are not in recentContributors
for(const username in currentTeamMembers){
if (!recentContributors[username]){
// Prior to deletion, confirm that member is on the 'base', i.e. 'website', team
// Prior to deletion, confirm that member is on the 'base' === 'website' team
const baseMember = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', {
org: org,
team_slug: baseTeam,
Expand All @@ -182,22 +267,28 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){
await octokit.request('PUT /orgs/{org}/teams/{team_slug}/memberships/{username}', {
org: org,
team_slug: baseTeam,
username: userName,
username: username,
role: 'member',
})
console.log('Member added to \'website\' team: ' + username);
}
// Remove member from the team if they don't pass additional checks in `shouldRemoveOrNotify` function
if(await shouldRemoveOrNotify(username)){
// But if member has an open issue, don't remove
if(username in inactiveWithOpenIssue){
cannotRemoveYet[username] = inactiveWithOpenIssue[username];
} else {
await octokit.request('DELETE /orgs/{org}/teams/{team_slug}/memberships/{username}', {
org: org,
team_slug: team,
username: username,
})
removedMembers.push(username);
}
}
// Remove contributor from a team if they don't pass additional checks in `toRemove` function
if(await toRemove(username)){
await octokit.request('DELETE /orgs/{org}/teams/{team_slug}/memberships/{username}', {
org: org,
team_slug: team,
username: username,
})
removedMembers.push(username);
}
}
}
return removedMembers;
return [removedMembers, cannotRemoveYet];
}


Expand All @@ -207,36 +298,32 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){
* @param {String} member - Member's username
* @returns {Boolean} - true/false
*/
async function toRemove(member){
// Collect user's repos and see if they recently joined hackforla/website;
// Note: user might have > 100 repos, the code below will need adjustment (see 'flip' pages);
const repos = await octokit.request('GET /users/{username}/repos', {
username: member,
per_page: 100
})

// If a user recently* cloned the 'website' repo (*within the last 30 days), then
// they are new members and are not considered for notification or removal.
for(const repository of repos.data){
// If repo is recently cloned, return 'false' so that member is not removed
if(repository.name === repo && repository.created_at > oneMonthAgo){
return false;
}
}

// Get user's membership status
const userMembership = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', {
async function shouldRemoveOrNotify(member){

// Get member's membership status: if member is a team 'Maintainer', return false- we don't remove maintainers
const membershipStatus = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', {
org: org,
team_slug: team,
username: member,
})

// If a user is a team 'maintainer', log their name and return 'false'. We do not remove maintainers
if(userMembership.data.role === 'maintainer'){
if(membershipStatus.data.role === 'maintainer'){
console.log("This inactive member is a 'Maintainer': " + member);
return false;
}
// Else this user is an inactive member of the team and should be notified or removed

// Run check to see if member cloned the 'website' repo within the last 30 days. If so do not notify
// because they are new members. (This will not catch new members who did not name their repos 'website'.)
try {
const repoData = await octokit.request('GET /repos/{username}/{repo}', {
username: member,
repo: repo,
});
if(repoData.created_at > oneMonthAgo){
return false;
}
} catch {}

// Else this member is inactive and should be notified or removed from team
return true;
}

Expand All @@ -254,8 +341,8 @@ async function notifyInactiveMembers(updatedTeamMembers, recentContributors){
// Loop over team members and add to "notify" list if they are not in recentContributors
for(const username in updatedTeamMembers){
if (!recentContributors[username]){
// Remove contributor from a team if they don't pass additional checks in `toRemove` function
if(await toRemove(username)){
// Check whether member should be added to notifiedMembers list
if(await shouldRemoveOrNotify(username)){
notifiedMembers.push(username)
}
}
Expand Down Expand Up @@ -284,8 +371,4 @@ function writeData(removedContributors, notifiedContributors){
console.log("File 'inactive-Members.json' saved successfully!");
});

fs.readFile('inactive-Members.json', (err, data) => {
if (err) throw err;
console.log("File 'inactive-Members.json' read successfully!");
});
}
}

This file was deleted.

Loading

0 comments on commit 3a2f3d9

Please sign in to comment.