This repository has been archived by the owner on May 23, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
205 lines (187 loc) · 7.13 KB
/
index.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
const core = require('@actions/core');
const github = require('@actions/github');
const octokit = new github.GitHub(core.getInput('github-token'));
const orgName = core.getInput('organization');
const parentTeam = core.getInput('parent-team');
const issueLabel = core.getInput('label');
const context = github.context;
// This code produces a weighted random selection of team in github to
// assign an issue to. The weight is based on the relative team size to
// the total population.
console.log(`Looking for teams in ${orgName}`);
octokit.
teams.
list({org: orgName}).
then(
(teamsResponse) => teamsWithParent(teamsResponse.data, parentTeam)
).then(
(teams) => attachAllTeamMembers(orgName, teams)
).then(
(teamsWithMembers) => attachTeamCounts(teamsWithMembers)
).then(
(teamsWithMembersAndCounts) => selectTeam(teamsWithMembersAndCounts)
).then(
(selectedTeam) => assignReviewTeamAndLabel(selectedTeam)
).catch(
(error) => core.setFailed(`Triage failed with: ${error.stack}`)
);
/**
* Selects the teams from an array of github team objects that
* have parent as a property.
*
* @param {!Array<Team>} teams Array of github team objects.
* @param {string} parentTeam The parent team to filter teams by.
* @return {!Array<Team>} Array of github team objects that pass the filter.
*/
function teamsWithParent(teams, parentTeam) {
console.log(`Looking for teams with ${parentTeam} as a parent.`);
return teams.filter(
team => team.parent != null && team.parent.name == parentTeam
);
}
/**
* Calls github to retrieves the team members in all the teams.
*
* @param {string} orgName name of the organization.
* @param {!Array<Team>} teams Array of github team objects.
* @return {!Promise<memberResponses} Array of promises that result in
* an array of teams with the team members.
*/
function attachAllTeamMembers(orgName, teams) {
console.log(`Attaching members to ${teams.length} teams.`);
// This builds up a promise that is fulfilled when all the promises within it
// are fulfilled. So, for each team, we build a promise that retrieves the
// members of the team, then attaches the response as a new property.
return Promise.all(
teams.map(
(team) => octokit.teams.listMembersInOrg({
org: orgName,
team_slug: team.slug
}).then(
(response) => {
// This is a way to do functional programming in javascript. The
// ... operator is like the ** in Ruby, and then adds a new property -
// members - to a newly created object.
return {
...team,
members: response.data
};
}
)
)
);
}
/**
* Accepts an array of team objects. These team objects are expected to have a
* members property containing user objects. The function attaches two fields:
* memberCount and cumulativeCount.
*
* memberCount is the number of team members in the team - adjusted for team
* members that may appear in more than one team in the array. In those
* cases, the number of team members that have appeared in groups earlier in
* the array. This is a greedy algorithm - the order of the team array impacts
* the result. For example, if TeamMember1 appears in both TeamA and TeamB,
* and TeamA occurs before TeamB in the array ([TeamA, TeamB]) then the
* adjustment will occur for the count of TeamB. If the order is reversed,
* then the adjustment will occur for the count of TeamA.
*
* cumulativeCount is the total number of team members counting from the first
* team in the teams array forward. This number is used to select teams
* proportionate to size. Again, it is dependent on the order of the teams
* and re-ordering the array will render the count meaningless.
*
* Note that this method currently mutates its inputs.
*
* @param {!Array<Team>} teams Array of github team objects with members
* property.
* @return {!Promise} A promise that returns the teams with the additional
* attirbutes.
*/
function attachTeamCounts(teams) {
// Some of the team members appear in more than one team, so this
// code keeps track of whom we have seen before and adjusts the
// counts accordingly
let seenMembers = new Set();
let cumulativeCount = 0;
return teams.map(
(team) => {
// Compute the number of times people appear in more than one
// team
let adjustCount = team.members.reduce(
(accumulator, teamMember) => {
if (seenMembers.has(teamMember.login)) {
accumulator++;
} else {
seenMembers.add(teamMember.login);
}
return accumulator;
},
0
);
let memberCount = team.members.length - adjustCount;
cumulativeCount += memberCount;
// Keep a running total of the team members. Note, that this
// implies we cannot reorder the teams array, because then the
// cumulative counts would be off. This should be fine for the
// purposes of this script, but if that changes in the future,
// then this code will need to be adjusted.
return {
...team,
memberCount: memberCount,
cumulativeCount: cumulativeCount
};
}
);
}
/**
* Selects a team from the teams array such that teams with more members
* get selected relatively more than teams with fewer members.
*
* @param {!Array<team>} teams Team objects with memberCount and cumulativeCount
* attached.
* @return {team object} The github team object selected for review.
*/
function selectTeam(teams) {
// So, now we have a list of teams with member counts. We also have
// a cumulativeCount on each team. This is going to allow us to
// weight selection using a uniform random number generator.
let totalTeamMembers = teams[teams.length - 1].cumulativeCount;
let selectedCap = Math.floor(totalTeamMembers * Math.random());
// We now have a selectedCap, and we can iterate through teams
// until we find a cumulativeCount >= than the cap. When this occurs,
// this is the team selected
let selectedTeam = teams.find(
(team) => team.cumulativeCount >= selectedCap
);
console.log(`Selected ${selectedTeam.name}`);
return selectedTeam;
}
/**
* Assigns a team to review the PR specified by the global context object. Also
* labels the PR.
*
* @param {team object} selectedTeam A github team object representing the team
* selected for assignment.
*/
function assignReviewTeamAndLabel(selectedTeam) {
let payload = context.payload;
console.log(`Creating review request for pull request ` +
`${payload.pull_request.number}`);
return octokit.pulls.createReviewRequest({
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: payload.pull_request.number,
team_reviewers: [selectedTeam.slug]
}).then(
(_reviewRequestResponse) => {
// Now we add a label to the issue to indicate that it requires QA.
console.log(`Adding label ${issueLabel} to issue.`);
return octokit.issues.addLabels({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.pull_request.number,
labels: [issueLabel]
});
}
);
}