-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
142 lines (127 loc) · 4.89 KB
/
main.ts
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
import { Temporal } from "@js-temporal/polyfill";
import fs from 'node:fs/promises';
import { IssueSummary, RepoSummary } from '../frontend/src/lib/repo-summaries.js';
import { browserSpecs } from "./browser-specs.js";
import { IssueOrPr, fetchAllComments, getRepo, logRateLimit } from "./github.js";
import { NeedsReporterFeedback, countAgendaTime, countSloTime, hasLabels, whichSlo } from "./slo.js";
import config from './third_party/config.cjs';
interface GlobalStatsInput {
totalRepos: number,
reposFinished: number;
}
async function analyzeRepo(org: string, repoName: string, globalStats: GlobalStatsInput): Promise<RepoSummary> {
const now = Temporal.Now.instant().round("second");
let result: RepoSummary | null = null;
try {
result = RepoSummary.parse(JSON.parse(await fs.readFile(`${config.outDir}/${org}/${repoName}.json`, { encoding: 'utf8' })));
} catch {
// On error, fetch the body.
}
if (!result || Temporal.Duration.compare(result.cachedAt.until(now), { hours: 22 }) > 0) {
const repo = await getRepo(org, repoName);
result = {
cachedAt: now,
org, repo: repoName,
issues: [],
labelsPresent: hasLabels(repo),
stats: {
numLabels: repo.labels.totalCount,
numIssues: repo.issues.totalCount,
numPRs: repo.pullRequests.totalCount,
}
};
const allIssues = repo.issues.nodes.concat(repo.pullRequests.nodes);
const needEarlyComments: IssueOrPr[] = [];
const needAllComments: IssueOrPr[] = [];
for (const issue of allIssues) {
// Fetch comments for the issues whose SLO calculation needs their comments.
if (issue.timelineItems.nodes.some(item =>
item.__typename === 'LabeledEvent' && NeedsReporterFeedback(item.label.name))) {
needAllComments.push(issue);
} else if (!result.labelsPresent && !issue.milestone) {
// We only need to see a few comments to see if someone other than the initial author has
// commented. This'll miss if the initial author has a long conversation with themself, but
// that should be rare.
needEarlyComments.push(issue);
}
}
await fetchAllComments(needAllComments, needEarlyComments);
for (const issue of allIssues) {
const info: IssueSummary = {
url: issue.url,
title: issue.title,
author: issue.author?.login,
createdAt: issue.createdAt,
sloTimeUsed: Temporal.Duration.from({ seconds: 0 }),
whichSlo: whichSlo(repo.nameWithOwner, issue),
onAgendaFor: countAgendaTime(issue, now),
labels: issue.labels.nodes.map(label => label.name),
stats: {
numTimelineItems: issue.timelineItems.totalCount,
numComments: issue.timelineItems.totalComments,
numLabels: issue.labels.totalCount,
}
};
if (issue.__typename === 'PullRequest') {
info.pull_request = { draft: issue.isDraft! };
}
if (issue.milestone) {
info.milestone = {
url: issue.milestone.url,
title: issue.milestone.title,
};
}
if (!result.labelsPresent && (
issue.milestone ||
issue.timelineItems.nodes.some(timelineItem =>
(timelineItem.__typename === 'IssueComment' ||
timelineItem.__typename === 'PullRequestReview' ||
timelineItem.__typename === 'PullRequestReviewThread')
&& info.author !== timelineItem.author?.login
))) {
// If the repository doesn't have enough triage labels, and an issue or PR has a comment
// from someone other than its creator, assume that person has also triaged the issue.
info.whichSlo = "none";
}
info.sloTimeUsed = countSloTime(issue, now, info.whichSlo);
result.issues.push(info);
};
}
await fs.mkdir(`${config.outDir}/${org}`, { recursive: true });
await fs.writeFile(`${config.outDir}/${org}/${repoName}.json`, JSON.stringify(result, undefined, 2));
globalStats.reposFinished++;
console.log(`[${globalStats.reposFinished}/${globalStats.totalRepos} ${new Date().toISOString()}] ${org}/${repoName}`);
return result;
}
async function main() {
const repos = new Set<string>();
for (const spec of await browserSpecs()) {
const repo = spec.nightly?.repository;
if (repo) {
repos.add(repo);
}
}
const githubRepos: { org: string, repo: string }[] = [];
for (const repoUrl of Array.from(repos).sort()) {
const url = new URL(repoUrl);
if (url.hostname !== 'github.com') {
continue;
}
const parts = url.pathname.split('/').filter(s => s);
if (parts.length !== 2) {
continue;
}
const [org, repo] = parts;
githubRepos.push({ org, repo });
}
logRateLimit();
const globalStats: GlobalStatsInput = {
totalRepos: githubRepos.length,
reposFinished: 0,
};
for (const { org, repo } of githubRepos) {
await analyzeRepo(org, repo, globalStats);
}
logRateLimit();
}
await main();