reviewable-enterprise-tools 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extract_data.js +83 -7
- package/lib/derivedInfo.js +28 -0
- package/lib/loadFirebase.js +3 -1
- package/lib/tokens.js +41 -0
- package/load_data.js +49 -2
- package/package.json +5 -1
- package/data.ndjson +0 -129
- package/repos.json +0 -1
package/extract_data.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
5
6
|
import * as zlib from 'zlib';
|
|
6
7
|
import commandLineArgs from 'command-line-args';
|
|
7
8
|
import getUsage from 'command-line-usage';
|
|
8
9
|
import {forEachLimit, forEachOfLimit, forEachOf} from 'async';
|
|
9
10
|
import nodefireModule from 'nodefire';
|
|
10
11
|
import {PromiseWritable} from 'promise-writable';
|
|
12
|
+
import {default as download} from 'download';
|
|
11
13
|
import Pace from 'pace';
|
|
14
|
+
import {uploadedFilesUrl, PLACEHOLDER_URL} from './lib/derivedInfo.js';
|
|
12
15
|
|
|
13
16
|
const NodeFire = nodefireModule.default;
|
|
14
17
|
|
|
@@ -20,6 +23,10 @@ const commandLineOptions = [
|
|
|
20
23
|
'user id mappings. (Optional, defaults to identity mapping.)'},
|
|
21
24
|
{name: 'output', alias: 'o', typeLabel: '{underline data.ndjson}',
|
|
22
25
|
description: 'Output ndJSON file for extracted data.'},
|
|
26
|
+
{name: 'download', alias: 'd', typeLabel: '{underline file/download/dir}',
|
|
27
|
+
description: 'Output directory for downloaded attachments'},
|
|
28
|
+
{name: 'logging', alias: 'l', type: Boolean,
|
|
29
|
+
description: 'Turn on low-level Firebase logging for debugging purposes'},
|
|
23
30
|
{name: 'help', alias: 'h', type: Boolean,
|
|
24
31
|
description: 'Display these usage instructions.'}
|
|
25
32
|
];
|
|
@@ -45,6 +52,17 @@ for (const property of ['repos', 'output']) {
|
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
let uploadedFilesUrlRegex;
|
|
56
|
+
if (uploadedFilesUrl) {
|
|
57
|
+
uploadedFilesUrlRegex = new RegExp(`(${_.escapeRegExp(uploadedFilesUrl)})([^)]*)`, 'g');
|
|
58
|
+
} else {
|
|
59
|
+
console.warn(
|
|
60
|
+
'WARNING: no REVIEWABLE_UPLOADS_PROVIDER or REVIEWABLE_UPLOADED_FILES_URL specified, ' +
|
|
61
|
+
'so not rewriting uploaded image URLs in comments.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (args.logging) NodeFire.enableFirebaseLogging(true);
|
|
65
|
+
|
|
48
66
|
const identityUserMap = !args.users;
|
|
49
67
|
const userMap = args.users ? JSON.parse(fs.readFileSync(args.users)) : {};
|
|
50
68
|
const repoNames =
|
|
@@ -55,14 +73,19 @@ const orgNames = _(repoNames).map(name => name.replace(/\/.*/, '')).uniq().value
|
|
|
55
73
|
const out = new PromiseWritable(fs.createWriteStream(args.output));
|
|
56
74
|
out.stream.setMaxListeners(Infinity);
|
|
57
75
|
|
|
58
|
-
const pace =
|
|
76
|
+
const pace = args.logging ?
|
|
77
|
+
{op() {}, total: 0} :
|
|
78
|
+
Pace(1 + 2 + orgNames.length + 2 * repoNames.length + _.size(userMap));
|
|
59
79
|
|
|
60
80
|
let reviewKeys = [];
|
|
61
81
|
const reversePullRequests = {};
|
|
62
82
|
let ghostedUsers = [];
|
|
63
83
|
const missingReviewKeys = [];
|
|
84
|
+
const brokenFiles = [];
|
|
85
|
+
const downloadedFiles = new Set();
|
|
64
86
|
|
|
65
87
|
async function extract() {
|
|
88
|
+
log('Connecting to Firebase');
|
|
66
89
|
await import('./lib/loadFirebase.js');
|
|
67
90
|
await extractSystem();
|
|
68
91
|
await extractOrganizations();
|
|
@@ -76,8 +99,15 @@ async function extract() {
|
|
|
76
99
|
await extractUsers();
|
|
77
100
|
await out.end();
|
|
78
101
|
pace.op();
|
|
102
|
+
console.log(
|
|
103
|
+
`Extracted ${orgNames.length} organizations, ${repoNames.length} repositories, ` +
|
|
104
|
+
`${reviewKeys.length} reviews, ${args.download ? '' : 'and '}${_.size(userMap)} users` + (
|
|
105
|
+
args.download ? `, and ${downloadedFiles.size - brokenFiles.length} files` : ''
|
|
106
|
+
)
|
|
107
|
+
);
|
|
79
108
|
logMissingReviews();
|
|
80
109
|
await logUnmappedUsers();
|
|
110
|
+
logBrokenFiles();
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
extract().then(() => {
|
|
@@ -115,8 +145,15 @@ function logMissingReviews() {
|
|
|
115
145
|
console.log(_(missingReviewKeys).map(key => reversePullRequests[key]).sort().join('\n'));
|
|
116
146
|
}
|
|
117
147
|
|
|
148
|
+
function logBrokenFiles() {
|
|
149
|
+
if (!args.download || !brokenFiles.length) return;
|
|
150
|
+
console.log(`\n${brokenFiles.length} files could not be downloaded:`);
|
|
151
|
+
console.log(brokenFiles.join('\n'));
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
async function extractSystem() {
|
|
119
|
-
|
|
155
|
+
log('Extracting /system');
|
|
156
|
+
const system = await db.child('system').get();
|
|
120
157
|
if (system.star && system.star !== '*' || system.bang && system.bang !== '!') {
|
|
121
158
|
throw new Error('Bad or missing REVIEWABLE_ENCRYPTION_AES_KEY');
|
|
122
159
|
}
|
|
@@ -128,6 +165,7 @@ async function extractSystem() {
|
|
|
128
165
|
|
|
129
166
|
async function extractOrganizations() {
|
|
130
167
|
if (!orgNames.length) return;
|
|
168
|
+
log('Extracting organizations');
|
|
131
169
|
await forEachLimit(orgNames, 5, async org => {
|
|
132
170
|
const organization = await db.child('organizations/:org', {org}).get();
|
|
133
171
|
await writeItem(`organizations/${toKey(org)}`, organization);
|
|
@@ -137,6 +175,7 @@ async function extractOrganizations() {
|
|
|
137
175
|
|
|
138
176
|
async function extractRepositories() {
|
|
139
177
|
if (!repoNames.length) return;
|
|
178
|
+
log('Extracting repositories');
|
|
140
179
|
await forEachLimit(repoNames, 10, async repoName => {
|
|
141
180
|
const [owner, repo] = repoName.split('/');
|
|
142
181
|
let repository = await db.child('repositories/:owner/:repo', {owner, repo}).get();
|
|
@@ -165,6 +204,7 @@ async function extractRepositories() {
|
|
|
165
204
|
|
|
166
205
|
async function extractRules() {
|
|
167
206
|
if (!repoNames.length) return;
|
|
207
|
+
log('Extracting rules');
|
|
168
208
|
await forEachLimit(repoNames, 10, async repoName => {
|
|
169
209
|
const [owner, repo] = repoName.split('/');
|
|
170
210
|
const rule = await db.child('rules/:owner/:repo', {owner, repo}).get();
|
|
@@ -175,6 +215,7 @@ async function extractRules() {
|
|
|
175
215
|
|
|
176
216
|
async function extractReviews() {
|
|
177
217
|
if (!reviewKeys.length) return;
|
|
218
|
+
log('Extracting reviews');
|
|
178
219
|
await forEachLimit(reviewKeys, 25, async reviewKey => {
|
|
179
220
|
let review = await db.child('reviews/:reviewKey', {reviewKey}).get();
|
|
180
221
|
if (review) {
|
|
@@ -184,12 +225,12 @@ async function extractReviews() {
|
|
|
184
225
|
const archive = await db.child('archivedReviews/:reviewKey', {reviewKey}).get();
|
|
185
226
|
if (archive) {
|
|
186
227
|
review = JSON.parse(zlib.gunzipSync(Buffer.from(archive.payload, 'base64')).toString());
|
|
187
|
-
stripReview(review);
|
|
228
|
+
const placeholdersPresent = stripReview(review);
|
|
188
229
|
if (identityUserMap) mapAllUserKeys(review);
|
|
189
230
|
archive.payload =
|
|
190
231
|
zlib.gzipSync(JSON.stringify(review), {level: zlib.constants.Z_BEST_COMPRESSION})
|
|
191
232
|
.toString('base64');
|
|
192
|
-
await writeItem(`archivedReviews/${reviewKey}`, archive);
|
|
233
|
+
await writeItem(`archivedReviews/${reviewKey}`, archive, {placeholdersPresent});
|
|
193
234
|
} else {
|
|
194
235
|
missingReviewKeys.push(reviewKey);
|
|
195
236
|
}
|
|
@@ -198,12 +239,33 @@ async function extractReviews() {
|
|
|
198
239
|
});
|
|
199
240
|
}
|
|
200
241
|
|
|
201
|
-
function stripReview(review) {
|
|
242
|
+
async function stripReview(review) {
|
|
243
|
+
let placeholderAdded = false;
|
|
202
244
|
review.core = _.omit(review.core, 'lastSweepTimestamp');
|
|
203
245
|
delete review.lastWebhook;
|
|
246
|
+
const downloadPromises = [];
|
|
204
247
|
review.discussions = _.pickBy(review.discussions, discussion => {
|
|
205
248
|
discussion.comments =
|
|
206
249
|
_.pickBy(discussion.comments, (comment, commentKey) => !/^gh-/.test(commentKey));
|
|
250
|
+
if (uploadedFilesUrl) {
|
|
251
|
+
_.forEach(discussion.comments, comment => {
|
|
252
|
+
if (!comment.markdownBody) return;
|
|
253
|
+
const body = comment.markdownBody.replace(uploadedFilesUrlRegex, (match, host, rest) => {
|
|
254
|
+
const url = host + rest;
|
|
255
|
+
if (args.download && !downloadedFiles.has(url)) {
|
|
256
|
+
downloadedFiles.add(url);
|
|
257
|
+
const dest = path.join(args.download, path.dirname(rest.slice(1)));
|
|
258
|
+
downloadPromises.push(download(url, dest).catch(e => {
|
|
259
|
+
if (args.logging) log(`File download failed:\n${url}\n${e}`);
|
|
260
|
+
brokenFiles.push(url);
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
return PLACEHOLDER_URL + rest;
|
|
264
|
+
});
|
|
265
|
+
if (body !== comment.markdownBody) placeholderAdded = true;
|
|
266
|
+
comment.markdownBody = body;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
207
269
|
return !_.isEmpty(discussion.comments);
|
|
208
270
|
});
|
|
209
271
|
if (_.isEmpty(review.discussions)) delete review.discussions;
|
|
@@ -219,10 +281,13 @@ function stripReview(review) {
|
|
|
219
281
|
return !_.isEmpty(sentiment.comments);
|
|
220
282
|
});
|
|
221
283
|
if (_.isEmpty(review.sentiments)) delete review.sentiments;
|
|
284
|
+
if (!_.isEmpty(downloadPromises)) await Promise.all(downloadPromises);
|
|
285
|
+
return placeholderAdded;
|
|
222
286
|
}
|
|
223
287
|
|
|
224
288
|
async function extractLinemaps() {
|
|
225
289
|
if (!reviewKeys.length) return;
|
|
290
|
+
log('Extracting linemaps');
|
|
226
291
|
await forEachLimit(reviewKeys, 25, async reviewKey => {
|
|
227
292
|
const linemap = await db.child('linemaps/:reviewKey', {reviewKey}).get();
|
|
228
293
|
await writeItem(`linemaps/${reviewKey}`, linemap);
|
|
@@ -232,6 +297,7 @@ async function extractLinemaps() {
|
|
|
232
297
|
|
|
233
298
|
async function extractFilemaps() {
|
|
234
299
|
if (!reviewKeys.length) return;
|
|
300
|
+
log('Extracting filemaps');
|
|
235
301
|
await forEachLimit(reviewKeys, 25, async reviewKey => {
|
|
236
302
|
const filemap = await db.child('filemaps/:reviewKey', {reviewKey}).get();
|
|
237
303
|
await writeItem(`filemaps/${reviewKey}`, filemap);
|
|
@@ -241,6 +307,7 @@ async function extractFilemaps() {
|
|
|
241
307
|
|
|
242
308
|
async function extractUsers() {
|
|
243
309
|
if (_.isEmpty(userMap)) return;
|
|
310
|
+
log('Extracting users');
|
|
244
311
|
await forEachOfLimit(userMap, 25, async (newUserKey, oldUserKey) => {
|
|
245
312
|
let user = await db.child('users/:oldUserKey', {oldUserKey}).get();
|
|
246
313
|
user = _.omit(
|
|
@@ -266,10 +333,15 @@ async function extractUsers() {
|
|
|
266
333
|
});
|
|
267
334
|
}
|
|
268
335
|
|
|
269
|
-
async function writeItem(key, value) {
|
|
336
|
+
async function writeItem(key, value, flags) {
|
|
270
337
|
if (value === undefined || value === null) return;
|
|
271
338
|
value = mapAllUserKeys(value, key);
|
|
272
|
-
|
|
339
|
+
if (flags) {
|
|
340
|
+
await out.write(
|
|
341
|
+
`[${JSON.stringify(key)}, ${JSON.stringify(value)}, ${JSON.stringify(flags)}]\n`);
|
|
342
|
+
} else {
|
|
343
|
+
await out.write(`[${JSON.stringify(key)}, ${JSON.stringify(value)}]\n`);
|
|
344
|
+
}
|
|
273
345
|
}
|
|
274
346
|
|
|
275
347
|
function mapAllUserKeys(object, context) {
|
|
@@ -306,3 +378,7 @@ function mapUserKey(userKey, context) {
|
|
|
306
378
|
function toKey(value) {
|
|
307
379
|
return NodeFire.escape(value);
|
|
308
380
|
}
|
|
381
|
+
|
|
382
|
+
function log(...params) {
|
|
383
|
+
if (args.logging) console.log('---', ...params);
|
|
384
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const uploadedFilesUrl = deriveUploadedFilesUrl();
|
|
2
|
+
export const PLACEHOLDER_URL = 'https://REVIEWABLE_UPLOADED_FILES.URL';
|
|
3
|
+
|
|
4
|
+
function deriveUploadedFilesUrl() {
|
|
5
|
+
if (process.env.REVIEWABLE_UPLOADED_FILES_URL) {
|
|
6
|
+
return process.env.REVIEWABLE_UPLOADED_FILES_URL.replace(/\/$/, '');
|
|
7
|
+
}
|
|
8
|
+
if (!process.env.REVIEWABLE_UPLOADS_PROVIDER) return;
|
|
9
|
+
switch (process.env.REVIEWABLE_UPLOADS_PROVIDER) {
|
|
10
|
+
case 'local':
|
|
11
|
+
return process.env.REVIEWABLE_HOST_URL + '/usercontent';
|
|
12
|
+
|
|
13
|
+
case 's3': {
|
|
14
|
+
let bucketUrl = 'https://s3.amazonaws.com/' + process.env.REVIEWABLE_S3_BUCKET;
|
|
15
|
+
if (process.env.AWS_REGION && process.env.AWS_REGION !== 'us-east-1') {
|
|
16
|
+
bucketUrl = bucketUrl.replace(/\/\/s3\./, '//s3-' + process.env.AWS_REGION + '.');
|
|
17
|
+
}
|
|
18
|
+
return bucketUrl;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case 'gcs':
|
|
22
|
+
return 'https://storage.googleapis.com/' + process.env.REVIEWABLE_GCS_BUCKET;
|
|
23
|
+
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unknown REVIEWABLE_UPLOADS_PROVIDER: ${process.env.REVIEWABLE_UPLOADS_PROVIDER}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/loadFirebase.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as bytes from 'bytes';
|
|
4
|
+
import * as path from 'path';
|
|
4
5
|
import admin from 'firebase-admin';
|
|
5
6
|
import nodefireModule from 'nodefire';
|
|
6
7
|
import {patchFirebase} from 'firecrypt';
|
|
@@ -73,7 +74,8 @@ if (process.env.REVIEWABLE_ENCRYPTION_AES_KEY) {
|
|
|
73
74
|
algorithm: 'aes-siv', key: process.env.REVIEWABLE_ENCRYPTION_AES_KEY,
|
|
74
75
|
cacheSize: FIRECRYPT_CACHE_SIZE
|
|
75
76
|
};
|
|
76
|
-
const specification = JSON.parse(fs.readFileSync(
|
|
77
|
+
const specification = JSON.parse(fs.readFileSync(
|
|
78
|
+
path.join(__dirname, '..', 'rules_firecrypt.json'), 'utf8'));
|
|
77
79
|
admin.database().configureEncryption(options, specification);
|
|
78
80
|
}
|
|
79
81
|
|
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import * as constants from 'constants';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
const keys = [];
|
|
6
|
+
|
|
7
|
+
export async function fetchToken(userKey) {
|
|
8
|
+
const encryptedToken = await db.child('users/:userKey/core/gitHubToken', {userKey}).get();
|
|
9
|
+
if (!encryptedToken) throw new Error(`User ${userKey} not signed in to Reviewable`);
|
|
10
|
+
if (!/^rsa\d*:/.test(encryptedToken)) return encryptedToken;
|
|
11
|
+
const cipherText = Buffer.from(encryptedToken.replace(/^rsa\d:/, ''), 'base64');
|
|
12
|
+
if (!process.env.REVIEWABLE_ENCRYPTION_PRIVATE_KEYS) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Unable to decrypt token for user ${userKey} without REVIEWABLE_ENCRYPTION_PRIVATE_KEYS`);
|
|
15
|
+
}
|
|
16
|
+
for (const key of keys) {
|
|
17
|
+
const token = crypto.privateDecrypt(key, cipherText).toString('utf8');
|
|
18
|
+
if (/^[ -~]+$/.test(token)) return token;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unable to decrypt token for user ${userKey} with any private key`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if (process.env.REVIEWABLE_ENCRYPTION_PRIVATE_KEYS) {
|
|
25
|
+
_.forEach(process.env.REVIEWABLE_ENCRYPTION_PRIVATE_KEYS.split(','), pemKey => {
|
|
26
|
+
const key = crypto.createPrivateKey(normalizePrivateKey(pemKey));
|
|
27
|
+
keys.push({key, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256'});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function normalizePrivateKey(pkcsKey) {
|
|
33
|
+
return pkcsKey.replace(
|
|
34
|
+
/-----BEGIN (.*?) KEY-----([\s\S]*?)-----END (\1) KEY-----/,
|
|
35
|
+
(match, keyType, contents) => {
|
|
36
|
+
return '-----BEGIN ' + keyType + ' KEY-----\n' +
|
|
37
|
+
contents.replace(/\\n|\s+/g, '').replace(/.{64}/g, '$&\n').replace(/\n*$/, '\n') +
|
|
38
|
+
'-----END ' + keyType + ' KEY-----\n';
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
package/load_data.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
|
+
import * as zlib from 'zlib';
|
|
5
6
|
import es from 'event-stream';
|
|
6
7
|
import commandLineArgs from 'command-line-args';
|
|
7
8
|
import getUsage from 'command-line-usage';
|
|
8
9
|
import nodefireModule from 'nodefire';
|
|
10
|
+
import Hubkit from 'hubkit';
|
|
9
11
|
import {PromiseReadable} from 'promise-readable';
|
|
10
12
|
import Pace from 'pace';
|
|
11
13
|
import {Throttle} from 'stream-throttle';
|
|
14
|
+
import {uploadedFilesUrl, PLACEHOLDER_URL} from './lib/derivedInfo.js';
|
|
15
|
+
import {fetchToken} from './lib/tokens.js';
|
|
12
16
|
|
|
13
17
|
const NodeFire = nodefireModule.default;
|
|
14
18
|
|
|
@@ -43,11 +47,21 @@ for (const property of ['input', 'admin']) {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
if (!process.env.REVIEWABLE_ENCRYPTION_AES_KEY) {
|
|
46
|
-
console.
|
|
50
|
+
console.warn('WARNING: not encrypting uploaded data as REVIEWABLE_ENCRYPTION_AES_KEY not given');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let placeholderUrlRegex, gh;
|
|
54
|
+
if (uploadedFilesUrl) {
|
|
55
|
+
placeholderUrlRegex = new RegExp(_.escapeRegExp(PLACEHOLDER_URL), 'g');
|
|
56
|
+
} else {
|
|
57
|
+
console.warn(
|
|
58
|
+
'WARNING: no REVIEWABLE_UPLOADS_PROVIDER or REVIEWABLE_UPLOADED_FILES_URL specified, ' +
|
|
59
|
+
'so not rewriting uploaded image URLs in comments.');
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
async function load() {
|
|
50
63
|
await import('./lib/loadFirebase.js');
|
|
64
|
+
if (uploadedFilesUrl) gh = new Hubkit({token: await fetchToken(args.admin)});
|
|
51
65
|
|
|
52
66
|
let sizeRead = 0;
|
|
53
67
|
let fatalError;
|
|
@@ -84,8 +98,21 @@ load().then(() => {
|
|
|
84
98
|
});
|
|
85
99
|
|
|
86
100
|
|
|
87
|
-
async function processLine([key, value]) {
|
|
101
|
+
async function processLine([key, value, flags]) {
|
|
102
|
+
if (uploadedFilesUrl && !_.isEmpty(value)) {
|
|
103
|
+
if (_.startsWith(key, 'reviews/')) {
|
|
104
|
+
await tweakReview(value);
|
|
105
|
+
} else if (_.startsWith(key, 'archivedReviews/') && flags?.placeholdersPresent) {
|
|
106
|
+
const review = JSON.parse(zlib.gunzipSync(Buffer.from(value.payload, 'base64')).toString());
|
|
107
|
+
await tweakReview(review);
|
|
108
|
+
value.payload =
|
|
109
|
+
zlib.gzipSync(JSON.stringify(review), {level: zlib.constants.Z_BEST_COMPRESSION})
|
|
110
|
+
.toString('base64');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
if (!_.isEmpty(value)) await db.child(key).update(value);
|
|
115
|
+
|
|
89
116
|
if (_.startsWith(key, 'reviews/')) {
|
|
90
117
|
const syncOptions = {
|
|
91
118
|
userKey: args.admin, prNumber: value.core.pullRequestId,
|
|
@@ -103,3 +130,23 @@ async function processLine([key, value]) {
|
|
|
103
130
|
}
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
async function tweakReview(review) {
|
|
134
|
+
const fullRepoName = `${review.core.ownerName}/${review.core.repoName}`;
|
|
135
|
+
const promises = [];
|
|
136
|
+
_.forEach(review.discussions, discussion => {
|
|
137
|
+
_.forEach(discussion.comments, comment => {
|
|
138
|
+
if (!comment.markdownBody) return;
|
|
139
|
+
const body = comment.markdownBody.replace(placeholderUrlRegex, uploadedFilesUrl);
|
|
140
|
+
if (body === comment.markdownBody) return;
|
|
141
|
+
comment.markdownBody = body;
|
|
142
|
+
promises.push(
|
|
143
|
+
gh.request('POST /markdown', {body: {
|
|
144
|
+
text: body, mode: 'gfm', context: fullRepoName
|
|
145
|
+
}}).then(htmlBody => {
|
|
146
|
+
comment.htmlBody = htmlBody;
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
if (promises.length) await Promise.all(promises);
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reviewable-enterprise-tools",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Admin tools for Reviewable Enterprise",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"read": "./read.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
+
"extract_data": "node ./extract_data.js",
|
|
13
|
+
"load_data": "node ./load_data.js",
|
|
12
14
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
15
|
},
|
|
14
16
|
"engines": {
|
|
@@ -36,9 +38,11 @@
|
|
|
36
38
|
"bytes": "^3.1.0",
|
|
37
39
|
"command-line-args": "^5.1.1",
|
|
38
40
|
"command-line-usage": "^6.1.0",
|
|
41
|
+
"download": "^8.0.0",
|
|
39
42
|
"event-stream": "^4.0.1",
|
|
40
43
|
"firebase-admin": "9.3.0",
|
|
41
44
|
"firecrypt": "^2.0.9",
|
|
45
|
+
"hubkit": "^3.0.0",
|
|
42
46
|
"lodash": "^4.17.20",
|
|
43
47
|
"ms": "^2.1.2",
|
|
44
48
|
"nodefire": "^3.0.0",
|