-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
267 lines (238 loc) · 8.99 KB
/
main.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
/**
* Copyright (c) Andrey Tolstoy <[email protected]>
* All rights reserved.
*
* This file is licensed under the BSD 2-Clause License, which accompanies this project
* and is available under https://opensource.org/licenses/BSD-2-Clause.
*/
'use strict';
import { version as PROJECT_VERSION } from './package.json';
import { AppError } from './error.js';
import Getopt from 'node-getopt';
import Octokit from '@octokit/rest';
import OctokitPluginRetry from '@octokit/plugin-retry';
import OctokitPluginThrottling from '@octokit/plugin-throttling';
import fs from 'fs';
import util from 'util';
Octokit.plugin(OctokitPluginRetry);
Octokit.plugin(OctokitPluginThrottling);
const GITHUB_RATE_LIMIT_MAX_RETRIES = 5;
const GITHUB_ABUSE_LIMIT_MAX_RETRIES = 5;
const getopt = Getopt.create([
['h', 'help', 'display this help'],
['v', 'version', 'show version'],
['t', 'token=TOKEN', 'GitHub token (can be also passed as GITHUB_TOKEN env variable)'],
['r', 'repo=REPO', 'GitHub repository (org/repo)'],
['p', 'pr=PR', 'GitHub Pull Request number'],
['', 'save[=SAVEFILE]', 'Save comments into a file (defaults to org_repo_pr_reviewId.json, e.g. avtolstoy_test_123_123456.json). This option is enabled by default'],
['', 'load=LOADFILE', 'Load comments from a file instead of fetching from GitHub']
]).bindHelp();
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function timeoutNotify(message, timeout) {
process.stdout.write(message + ' ');
do {
process.stdout.write(`${timeout}`);
await sleep(250);
process.stdout.write('.');
await sleep(250);
process.stdout.write('.');
await sleep(250);
process.stdout.write(' ');
await sleep(250);
} while (--timeout);
console.log('');
}
async function saveComments(saveFile, comments) {
try {
console.log(`Saving comments into a file ${saveFile}`);
await fs.promises.writeFile(saveFile, JSON.stringify(comments), 'utf-8');
console.log('Saved');
} catch (e) {
throw new AppError('Failed to save comments into a file', e);
}
}
export async function main() {
const requiredArgs = ['repo', 'pr'];
const opt = getopt.parseSystem();
// Print version and exit
if ('version' in opt.options) {
console.info(PROJECT_VERSION);
return;
}
// Check for required arguments
if (!requiredArgs.every((v) => Object.keys(opt.options).includes(v))) {
throw new AppError('No repository and PR number provided');
}
// Validate repo and PR
if (opt.options.repo.indexOf('/') === -1 || isNaN(opt.options.pr)) {
throw new AppError('No valid repository and PR number provided');
}
// Check for GitHub token
const githubToken = opt.options.token || process.env.GITHUB_TOKEN;
if (!githubToken || !githubToken.length) {
throw new AppError('No GitHub token provided');
}
const [org, repo] = opt.options.repo.split('/');
const pr = +opt.options.pr;
const github = new Octokit({
auth: githubToken,
throttle: {
onRateLimit: (retryAfter, options) => {
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`)
if (options.request.retryCount < GITHUB_RATE_LIMIT_MAX_RETRIES) {
console.log(`Retrying after ${retryAfter} seconds`);
return true;
}
},
onAbuseLimit: (retryAfter, options) => {
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
if (options.request.retryCount < GITHUB_ABUSE_LIMIT_MAX_RETRIES) {
console.log(`Retrying after ${retryAfter} seconds`);
return true;
}
}
}
});
// Get our userId
console.log('Fetching user info');
let userId;
try {
const auth = await github.users.getAuthenticated();
userId = auth.data.id;
console.log(`We are '${auth.data.login}' (${userId})`);
} catch (e) {
throw new AppError('Failed to fetch user info', e);
}
// Get the list of reviews for the PR and get the one in PENDING state
console.log(`Fetching list of reviews for PR ${pr}`);
let reviewId;
try {
const listReviewsReq = github.pulls.listReviews.endpoint.merge({
owner: org,
repo: repo,
pull_number: pr
});
for await (const response of github.paginate.iterator(listReviewsReq)) {
for (const review of response.data) {
if (review.user.id === userId && review.state === 'PENDING') {
if (reviewId !== undefined) {
throw new AppError('More than one pending review?');
}
reviewId = review.id;
}
}
}
} catch (e) {
throw new AppError(`Failed to fetch list of reviews for PR ${pr}`, e);
}
if (reviewId) {
console.log(`Found a pending review from us: ${reviewId}`);
}
let saveFile = opt.options.save;
if (!saveFile) {
if (reviewId) {
saveFile = `${org}_${repo}_${pr}_${reviewId}.json`;
} else if (opt.options.load) {
saveFile = opt.options.load;
} else {
saveFile = `${org}_${repo}_${pr}_sync.json`;
}
}
let comments;
if (reviewId && !('load' in opt.options)) {
console.log(`Fetching a list of comments for the review ${reviewId}`);
try {
const getCommentsForReviewReq = github.pulls.getCommentsForReview.endpoint.merge({
owner: org,
repo: repo,
pull_number: pr,
review_id: reviewId
});
comments = await github.paginate(getCommentsForReviewReq) || [];
} catch (e) {
throw new AppError(`Failed to fetch a list of comments for the review ${reviewId}`, e);
}
// Save into a file
await saveComments(saveFile, comments);
} else if (opt.options.load) {
console.log(`Loading comments from file '${opt.options.load}'`);
try {
const r = await fs.promises.readFile(opt.options.load);
comments = JSON.parse(r);
} catch (e) {
throw new AppError(`Failed to load comments from file '${opt.options.load}'`);
}
} else {
throw new AppError(`No pending review for PR ${pr} and no --load option specified`);
}
if (reviewId) {
console.warn('The current pending review will be dismissed, removing all the pending comments');
console.warn('This is a destructive but necessary action, because it is impossible to post comments on a PR with a pending review');
console.warn('The comments have also been saved to a file and can be loaded from it, in case something goes wrong');
await timeoutNotify(`Dismissing review ${reviewId} in`, 10);
try {
const rep = await github.pulls.deletePendingReview({
owner: org,
repo: repo,
pull_number: pr,
review_id: reviewId
});
} catch (e) {
throw new AppError(`Failed to dismiss review ${reviewId}`, e);
}
}
console.log(`Will be posting ${comments.length} pending review comments as regular comments`);
for (const comment of comments) {
if (!comment.new_id) {
console.log(`Posting a comment for ${comment.commit_id.slice(0, 7)} ${comment.path}:${comment.position} (${comment.id})`);
} else {
// Comment has already been posted, skipping
console.log(`Skipping an already posted comment for ${comment.commit_id.slice(0, 7)} ${comment.path}:${comment.position} (${comment.id})`);
continue;
}
try {
const pResp = await github.pulls.createComment({
owner: org,
repo: repo,
pull_number: pr,
body: comment.body,
commit_id: comment.commit_id,
path: comment.path,
position: comment.position
});
console.log(`Posted, new id: ${pResp.data.id}`);
comment.new_id = pResp.data.id;
} catch (e) {
console.warn('Failed:', e.message);
}
}
// Re-save to keep track of already posted comments
await saveComments(saveFile, comments);
}
export function showHelp() {
getopt.showHelp();
}
export async function run() {
try {
await main();
} catch (e) {
if (e instanceof AppError) {
console.error('Error:', e.message);
if (!e.extra) {
showHelp();
} else {
console.error(e.extra);
}
} else {
console.error(e);
console.error(e.stack);
}
process.exit(1);
}
}
// Re-export
export { AppError };