-
Notifications
You must be signed in to change notification settings - Fork 3
/
githubData.js
446 lines (433 loc) · 15 KB
/
githubData.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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
import gql from 'graphql-tag';
// TODO: split query into search + virtualized table to improve caching
// TODO: implement query pagination to allow getting more than 100 items
// TODO: more precise filtering of the timeline items (only checking for comments right now)
/**
* Fragment indicating the values and structure of all issue objects fetched
* using the github queries below.
*/
const ISSUE_FRAGMENT = gql`
fragment GetIssueInfo on Issue {
id
title
authorAssociation
author {
login
url
}
repository {
name
url
}
labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
nodes {
name
color
}
}
number
url
createdAt
}
`;
/**
* Fragment indicating the values and structure of all PR objects fetched using
* the github queries below.
*/
const PR_FRAGMENT = gql`
fragment GetPRInfo on PullRequest {
id
title
authorAssociation
author {
login
url
}
repository {
name
url
}
labels(first: 100, orderBy: { field: NAME, direction: ASC }) {
nodes {
name
color
}
}
number
url
createdAt
}
`;
/**
* Query to perform a GitHub search and fetch all "Issues" (Issues and PRs) that
* match a given query. We can use this graphQL query and GitHub's powerful
* search syntax to only fetch items which were created by users who are not
* employees. Use makeNewSearch to construct a search query paramter that finds
* PRs and Issues that are open and not created/commented by an employee.
*
* Note that due to the nature of the typically large search query, this GraphQL
* query may take a few seconds to execute.
*
* @param {string} query The GitHub search query to search with.
*/
const SEARCH_NEW_ITEMS_QUERY = gql`
query SearchResults($query: String!) {
search(query: $query, type: ISSUE, first: 100) {
nodes {
__typename
...GetIssueInfo
...GetPRInfo
}
issueCount
}
rateLimit {
limit
cost
remaining
resetAt
}
}
${PR_FRAGMENT}
${ISSUE_FRAGMENT}
`;
/**
* Query to perform a GitHub search and fetch all "Issues" (Issues and PRs) that
* match several given queries. We can use this graphQL query and GitHub's
* powerful search syntax to only fetch items which are considered stale.
*
* In this case, a "stale" item is an item that received a response from an
* employee but then received no other response from an employee for at least a
* certain duration. GitHub's search syntax doesn't quite allow us to get all
* this information in a single search, so the query is split into two separate
* searches:
* * definitelyStale - Issues or PRs that were not created by an employee, have
* at least one comment from an employee, and were last updated before the
* stale threshold. "Last updated" in GitHub terms can mean a variety of things
* (including comments from other users), so this query doesn't catch issues or
* PRs that are active but haven't received an employee response in awhile. The
* query for this search is generated by makeDefStaleSearch.
* * maybeStale - Issues or PRs that were not created by a an employee, and were
* created before the stale threshold but updated after it. As GitHub search
* cannot determine if an employee has commented within a certain time, this
* search fetches the TimeLine items of each issue/pr candidate, expecting the
* application to use this data to determine which Issues/PRs have received
* employee follow up. The query for this search is generated by
* makeDefStaleSearch, and the results from this search can be filtered using
* filterMaybeStaleItems.
*
* Note that due to the nature of the typically large search query, this GraphQL
* query may take a few seconds to execute.
*
* @param {string} queryDefStale The GitHub search query to search for
* definitelyStale items. Use makeDefStaleSearch to make this query.
* @param {string} queryMaybeStale The GitHub search query to search for
* maybeStale items. Use makeMaybeStaleSearch to make this query.
* @param {string} timeSince An ISO Date string indicating the threshold to use
* for considering items to be stale (ex. two weeks before today).
*/
const SEARCH_STALE_ITEMS_QUERY = gql`
query SearchResults(
$queryDefStale: String!
$queryMaybeStale: String!
$timeSince: DateTime!
) {
definitelyStale: search(query: $queryDefStale, type: ISSUE, first: 100) {
issueCount
nodes {
__typename
...GetIssueInfo
...GetPRInfo
}
}
maybeStale: search(query: $queryMaybeStale, type: ISSUE, first: 100) {
issueCount
nodes {
__typename
...GetIssueInfo
...GetPRInfo
... on Issue {
timelineItems(since: $timeSince, last: 100) {
nodes {
... on Comment {
id
author {
login
}
updatedAt
}
}
}
}
... on PullRequest {
timelineItems(since: $timeSince, last: 100) {
nodes {
... on Comment {
id
author {
login
}
updatedAt
}
}
}
}
}
}
rateLimit {
limit
cost
remaining
resetAt
}
}
${PR_FRAGMENT}
${ISSUE_FRAGMENT}
`;
/**
* Utility function to create a search query that looks for items that haven't
* received an employee response yet. See SEARCH_NEW_ITEMS_QUERY for more
* information on how to use this function.
*
* The returned query finds Issues/PRs matching the following criteria:
* * On a repository in `repos`
* * Was not authored or commented on by anyone in `users`
* * Does not have a label in `ignoreLabels`
* * Is currently open
*
* @param {string[]} users GitHub logins (usernames, not emails) to treat as
* employees.
* @param {string[]} repos Repositories to pull Issues and PRs from.
* @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
* Issues and PRs with any of these labels will be excluded from results.
* @returns {string} A GitHub search query to use as an input to
* SEARCH_NEW_ITEMS_QUERY
*/
function makeNewSearch(users, repos, ignoreLabels) {
return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
.map((u) => `-author:${u} -commenter:${u}`)
.join(' ')} ${ignoreLabels.map((l) => `-label:${l}`).join(' ')} is:open`;
}
/**
* Utility function to create a search query that looks for items that are
* definitely stale. See SEARCH_STALE_ITEMS_QUERY for more information on how
* to use this function.
*
* The returned query finds Issues/PRs matching the following criteria:
* * On a repository in `repos`
* * Was not authored anyone in `users`
* * Has received a comment by someone in `users`
* * Does not have a label in `ignoreLabels`
* * Is currently open
* * Was last updated before or on `date`.
*
* @param {string[]} users GitHub logins (usernames, not emails) to treat as
* employees.
* @param {string[]} repos Repositories to pull Issues and PRs from.
* @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
* Issues and PRs with any of these labels will be excluded from results.
* @param {Date} date A Date to treat as the threshold to considering an item to
* be stale (ex. two weeks before today).
* @returns {string} A GitHub search query to use as an input to
* SEARCH_STALE_ITEMS_QUERY
*/
function makeDefStaleSearch(users, repos, ignoreLabels, date) {
return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
.map((u) => `-author:${u} commenter:${u}`)
.join(' ')} ${ignoreLabels
.map((l) => `-label:${l}`)
.join(' ')} is:open updated:<=${date.toISOString()}`;
}
/**
* Utility function to create a search query that looks for items that might be
* stale. See SEARCH_STALE_ITEMS_QUERY for more information on how to use this
* function.
*
* The returned query finds Issues/PRs matching the following criteria:
* * On a repository in `repos`
* * Was not authored anyone in `users`
* * Has received a comment by someone in `users`
* * Does not have a label in `ignoreLabels`
* * Is currently open
* * Was last updated after `date`
* * Was created before or on `date`
*
* @param {string[]} users GitHub logins (usernames, not emails) to treat as
* employees.
* @param {string[]} repos Repositories to pull Issues and PRs from.
* @param {string[]} ignoreLabels Labels (ex. "bug") to ignore when searching.
* Issues and PRs with any of these labels will be excluded from results.
* @param {Date} date A Date to treat as the threshold to considering an item to
* be stale (ex. two weeks before today).
* @returns {string} A GitHub search query to use as an input to
* SEARCH_STALE_ITEMS_QUERY
*/
function makeMaybeStaleSearch(users, repos, ignoreLabels, date) {
return `${repos.map((r) => `repo:${r}`).join(' ')} ${users
.map((u) => `-author:${u} commenter:${u}`)
.join(' ')} ${ignoreLabels
.map((l) => `-label:${l}`)
.join(
' '
)} is:open updated:>${date.toISOString()} created:<=${date.toISOString()}`;
}
/**
* Filter SEARCH_STALE_ITEMS_QUERY.maybeStale.nodes based on which users are
* considered employees. This function checks the `timelineItems` to see if the
* Issue/PR has received a comment from an employee after the date in which it
* is considered stale, and returns only items that are stale by this test.
*
* @param {{ timelineItems: { nodes: any[] } }[]} items An array of Issues/PRs
* with a timeline
* @param {string[]} employeeLogins A list of GitHub logins to treat as
* employees
* @param {Date} staleDate A date to treat as the threshold of being stale (ex.
* two weeks before today).
* @returns {{ timelineItems: { nodes: any[] } }[]} The items paramter array
* with the stale Issues/PRs removed.
*/
function filterMaybeStaleItems(items, employeeLogins, staleDate) {
const employeeSet = new Set(employeeLogins);
return items.filter((n) =>
n.timelineItems.nodes.every((c) =>
c?.author && employeeSet.has(c.author.login)
? new Date(c.updatedAt) <= staleDate
: true
)
);
}
/**
* Get some useful information about the user we are currently authenticated as.
* This query is used to both test the authentication mechanism and fetch
* information about the account that can be used to provide helpful
* suggestions in the settings.
*
* @param {?string} repoCursor An endCursor to paginate over the repositories
* with.
*/
const GET_CUR_USER_INFO_QUERY = gql`
query($repoCursor: String) {
viewer {
login
repositories(
affiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]
first: 100
after: $repoCursor
) {
nodes {
nameWithOwner
}
}
}
}
`;
/**
* Run a GraphQL query to get information about new and stale items for a given
* set of repositories. In this case, new items are Issues/PRs that have not
* received a response from an employee, and stale items are Issues/PRs that
* have received an employee response but have not received a follow up for
* longer than a certain period of time. For more information about the exact
* process used to determine which item is new/stale, check out the
* SEARCH_NEW_ITEMS_QUERY and SEARCH_STALE_ITEMS_QUERY queries.
*
* Note: this function currently is limited to 100 items per new/stale. This
* will be fixed when query pagination is implemented.
*
* @param {any} client Apollo GraphQL client to use to query the GitHub GraphQL
* API. Must be preloaded with the proper credentials.
* @param {string[]} options.scanRepos The repositories to scan, in "org/name"
* format.
* @param {string[]} options.companyUsers Login names of GitHub accounts
* associated with employees. This value is used to determine which items
* have received a response from someone in the company. Login names should be
* usernames not emails (ex. "prototypicalpro")
* @param {string[]} options.ignoreLabels Issue/PR labels to exclude (ex."bug").
* All issues/PRs with these labels will be excluded from the results.
* @param {number} options.staleTime Duration in milliseconds that a item should
* remain inactive for it to be considered stale.
* @returns {{
* newSearchCount: number;
* newSearchItems: object[];
* staleSearchCount: number;
* staleSearchItems: object[];
* }}
* An object containing the results of the queries. The "items" arrays will contain objects of either
* ISSUE_FRAGMENT or PR_FRAGMENT structure.
*/
export async function findDashboardItems(client, options) {
const { scanRepos, companyUsers, staleTime, ignoreLabels } = options;
const staleDate = new Date(Date.now() - staleTime);
// fetch all the data
const [newRes, staleRes] = await Promise.all([
client.query({
query: SEARCH_NEW_ITEMS_QUERY,
variables: {
query: makeNewSearch(companyUsers, scanRepos, ignoreLabels),
},
}),
client.query({
query: SEARCH_STALE_ITEMS_QUERY,
variables: {
queryDefStale: makeDefStaleSearch(
companyUsers,
scanRepos,
ignoreLabels,
staleDate
),
queryMaybeStale: makeMaybeStaleSearch(
companyUsers,
scanRepos,
ignoreLabels,
staleDate
),
timeSince: staleDate.toISOString(),
},
}),
]);
// if every comment by an employee is stale, then the issue is stale
const filteredMaybeItems = filterMaybeStaleItems(
staleRes.data.maybeStale.nodes,
companyUsers,
staleDate
);
return {
newSearchCount: Math.max(
newRes.data.search.issueCount,
newRes.data.search.nodes.length
),
newSearchItems: newRes.data.search.nodes,
staleSearchCount:
Math.max(
staleRes.data.definitelyStale.issueCount,
staleRes.data.definitelyStale.nodes.length
) + filteredMaybeItems.length,
staleSearchItems: staleRes.data.definitelyStale.nodes.concat(
filteredMaybeItems
),
};
}
/**
* Get some useful information about the user we are currently authenticated as.
* This function can both test the authentication mechanism and fetch
* information about the account that can be used to provide helpful
* suggestions in the settings.
*
* @param {any} client Apollo GraphQL client to use to query the GitHub GraphQL
* API.
* @param {string} token GitHub personal access token to use for authenticating
* this request
* @returns {Promise} A promise containing the result of the
* GET_CUR_USER_INFO_QUERY. Will thrown an exception if the supplied token
* is invalid.
*/
export async function getUserInfo(client, token) {
const { data } = await client.query({
query: GET_CUR_USER_INFO_QUERY,
fetchPolicy: 'network-only',
context: {
headers: {
authorization: `Bearer ${token}`,
},
},
});
return data;
}