-
-
Notifications
You must be signed in to change notification settings - Fork 776
/
get-contributors-data.js
185 lines (159 loc) · 7.97 KB
/
get-contributors-data.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Import modules
const getTimeline = require('../../utils/get-timeline');
const getTeamMembers = require('../../utils/get-team-members');
// Global variables
var github;
var context;
const maintTeam = 'website-maintain';
const botMembers = ['elizabethhonest', 'hfla-website-checklist', 'HackforLABot'];
// Set date limits: we are sorting inactive members into groups to notify after 1 month and remove after 2 months.
// Since the teams take off December and July, the Jan. 1st and Aug. 1st runs are skipped (via `schedule-monthly.yml`).
// The Feb. 1st and Sept. 1st runs account for skipped months: 'oneMonth' & 'twoMonths' = 1 & 3 months respectively
let today = new Date();
let twoMonths = (today.getMonth() === 1 || today.getMonth() === 8) ? 3 : 2;
let oneMonthAgo = new Date(); // oneMonthAgo instantiated with date of "today"
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); // 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"
twoMonthsAgo = twoMonthsAgo.toISOString();
let dates = [oneMonthAgo, twoMonthsAgo];
/**
* Main function
* @param {Object} g - github object from actions/github-script
* @param {Object} c - context object from actions/github-script
* @return {Object} results - object to use in `trim-inactive-members.js`
*/
async function main({ g, c }) {
github = g;
context = c;
const [contributorsOneMonthAgo, contributorsTwoMonthsAgo, inactiveWithOpenIssue] = await fetchContributors(dates);
console.log(`-`.repeat(60));
console.log('List of active contributors since ' + dates[0].slice(0, 10) + ':');
console.log(contributorsOneMonthAgo);
return {
recentContributors: contributorsOneMonthAgo,
previousContributors: contributorsTwoMonthsAgo,
inactiveWithOpenIssue: inactiveWithOpenIssue,
dates: dates,
};
};
/**
* Function to fetch list of contributors with comments/commits/issues since date
* @returns {Object} allContributorsSinceOneMonthAgo - List of active contributors since oneMonthAgo
* @returns {Object} allContributorsSinceTwoMonthsAgo - List of active contributors since twoMonthsAgo
*/
async function fetchContributors(dates){
let allContributorsSinceOneMonthAgo = {};
let allContributorsSinceTwoMonthsAgo = {};
let inactiveWithOpenIssue = {};
// Members of 'website-maintain' team are considered permanent members
const permanentMembers = await getTeamMembers(github, context, maintTeam);
// Fetch all contributors with commit, comment, and issue (assignee) contributions
const APIs = [
'GET /repos/{owner}/{repo}/commits', // Gets list of member commits for dates
'GET /repos/{owner}/{repo}/issues/comments', // Gets list of member comments for dates
'GET /repos/{owner}/{repo}/issues' // Gets list of member assignments for dates
];
for (const date of dates){
const allContributorsSince = {};
for(const api of APIs){
let pageNum = 1;
let result = [];
// Since Github only allows to fetch max 100 items per request, we need to 'flip' pages
while(true){
// Fetch 100 items per each page (`pageNum`)
const contributors = await github.request(api, {
owner: context.repo.owner,
repo: context.repo.repo,
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`)
if(!contributors.data.length){
break;
} else {
result = result.concat(contributors.data);
pageNum++;
}
}
// Once we have looked at all pages and collected all the data, we create key-value pairs of recent contributors and store
// them in the `allContributorsSince` object. The contributor data that comes back from each API are stored differently,
// i.e. `author.login` vs `user.login` vs `assignee.login`. We want to extract the contributors' usernames for each situation.
for(const contributorInfo of result){
// Check if username is stored in `author.login`
if(contributorInfo.author){
allContributorsSince[contributorInfo.author.login] = true;
}
// Check for usernames in `user.login`, but only include `created_at` time, and skip `user.login` b/c covered by 3rd API
else if(contributorInfo.user && contributorInfo.created_at > date && 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.
// 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 getTimeline(issueNum, github, context);
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, add to open issues with inactive. If issue title
// includes "Skills Issue" or "Pre-work Checklist", set flag to true, otherwise set to false
else if (date === dates[1]) {
const regex = /Pre-work checklist|Skills Issue/i;
if (regex.test(contributorInfo.title)) {
inactiveWithOpenIssue[assignee] = [issueNum, true];
} else {
inactiveWithOpenIssue[assignee] = [issueNum, false];
}
}
}
}
}
// Add permanent members from 'website-maintain' team to list of active contributors
for(let permanentMember in permanentMembers){
allContributorsSince[permanentMember] = true;
}
// Add members of botMembers team to list of active contributors
for(let i = 0; i < botMembers.length; i++){
allContributorsSince[botMembers[i]] = true;
}
if(date === dates[0]){
allContributorsSinceOneMonthAgo = allContributorsSince;
} else {
allContributorsSinceTwoMonthsAgo = allContributorsSince;
}
}
return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo, inactiveWithOpenIssue];
}
/**
* Helper function for fetchContributors()
* @param {String} date - date: oneMonthAgo, twoMonthsAgo
* @param {Object} timeline - object issue event timeline
* @param (String} assignee - member assigned to issue
* @returns {Boolean} true/false - whether event occurred after date
*/
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 };
}
module.exports = main;