Skip to content
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

Consolidate functions in schedule-monthly.yml 5969 #6011

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
787f8b1
Create inactive-members.md
t-will-gillis Dec 5, 2023
b5b6fa7
Update inactive-members.md
t-will-gillis Dec 5, 2023
7b1adad
Update create-new-issue.js
t-will-gillis Dec 5, 2023
bece8be
Create issue-template-parser.js
t-will-gillis Dec 5, 2023
a1b794d
Update issue-template-parser.js
t-will-gillis Dec 5, 2023
ef2506f
Update inactive-members.md
t-will-gillis Dec 5, 2023
cd36c7a
Update create-new-issue.js
t-will-gillis Dec 5, 2023
e2c63f1
Update inactive-members.md
t-will-gillis Dec 8, 2023
dcd17b5
Update issue-template-parser.js
t-will-gillis Dec 17, 2023
1210045
Update contributors-data.js
t-will-gillis Dec 17, 2023
0d9fb19
Update contributors-data.js
t-will-gillis Dec 17, 2023
d510214
Update contributors-data.js
t-will-gillis Dec 17, 2023
16c42ec
Update create-new-issue.js
t-will-gillis Dec 17, 2023
a499c06
Update contributors-data.js
t-will-gillis Dec 17, 2023
ae453f4
Update contributors-data.js
t-will-gillis Dec 21, 2023
1c05c05
Update check-labels.js
t-will-gillis Dec 28, 2023
59cc1ce
Update contributors-data.js
t-will-gillis Jan 7, 2024
f7745f4
Merge branch 'gh-pages' into consolidate-funct-sch-month-5969
t-will-gillis Jan 10, 2024
aebb0e1
Update check-labels.js
t-will-gillis Jan 10, 2024
f8ee91f
Update contributors-data.js
t-will-gillis Jan 15, 2024
e7b292c
Update contributors-data.js
t-will-gillis Jan 15, 2024
1061b1b
Update contributors-data.js
t-will-gillis Jan 17, 2024
fbd02d5
Update contributors-data.js
t-will-gillis Jan 19, 2024
66b7e84
Update schedule-monthly.yml
t-will-gillis Jan 20, 2024
e1d6ad1
Update create-new-issue.js
t-will-gillis Jan 20, 2024
1d600f4
Delete github-actions/trigger-schedule/list-inactive-members/comment-…
t-will-gillis Jan 20, 2024
a12e050
Update create-new-issue.js
t-will-gillis Jan 20, 2024
dbf8621
Update contributors-data.js
t-will-gillis Jan 20, 2024
724341f
Update contributors-data.js
t-will-gillis Jan 20, 2024
bf7d139
Update schedule-monthly.yml
t-will-gillis Jan 24, 2024
d9dd935
Update contributors-data.js
t-will-gillis Jan 24, 2024
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
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