@capraconsulting/cals-cli 3.13.1 → 3.15.0
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/README.md +42 -33
- package/lib/cals-cli.mjs +714 -2497
- package/lib/cals-cli.mjs.map +1 -1
- package/lib/cli/commands/{github/sync.d.ts → sync.d.ts} +1 -1
- package/lib/cli/reporter.d.ts +5 -6
- package/lib/cli/util.d.ts +1 -6
- package/lib/config.d.ts +0 -2
- package/lib/definition/index.d.ts +1 -1
- package/lib/definition/types.d.ts +0 -52
- package/lib/github/index.d.ts +0 -2
- package/lib/github/service.d.ts +2 -67
- package/lib/github/types.d.ts +0 -63
- package/lib/github/util.d.ts +0 -1
- package/lib/index.d.ts +0 -9
- package/lib/index.es.js +8 -1323
- package/lib/index.es.js.map +1 -1
- package/lib/index.js +8 -1323
- package/lib/index.js.map +1 -1
- package/package.json +7 -19
- package/lib/cli/commands/definition/util.d.ts +0 -6
- package/lib/cli/commands/definition/util.test.d.ts +0 -1
- package/lib/cli/commands/github/analyze-directory.d.ts +0 -3
- package/lib/cli/commands/github/configure.d.ts +0 -3
- package/lib/cli/commands/github/generate-clone-commands.d.ts +0 -3
- package/lib/cli/commands/github/list-pull-requests-stats.d.ts +0 -3
- package/lib/cli/commands/github/list-repos.d.ts +0 -3
- package/lib/cli/commands/github/list-webhooks.d.ts +0 -3
- package/lib/cli/commands/github/set-token.d.ts +0 -3
- package/lib/cli/commands/github/util.d.ts +0 -3
- package/lib/cli/commands/github.d.ts +0 -3
- package/lib/cli/commands/snyk/report.d.ts +0 -3
- package/lib/cli/commands/snyk/set-token.d.ts +0 -3
- package/lib/cli/commands/snyk/sync.d.ts +0 -3
- package/lib/cli/commands/snyk.d.ts +0 -3
- package/lib/github/changeset/changeset.d.ts +0 -21
- package/lib/github/changeset/execute.d.ts +0 -10
- package/lib/github/changeset/types.d.ts +0 -88
- package/lib/snyk/index.d.ts +0 -3
- package/lib/snyk/service.d.ts +0 -30
- package/lib/snyk/token.d.ts +0 -11
- package/lib/snyk/types.d.ts +0 -62
- package/lib/snyk/util.d.ts +0 -3
- package/lib/snyk/util.test.d.ts +0 -1
- package/lib/testing/executor.d.ts +0 -25
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/lib.d.ts +0 -63
- /package/lib/cli/commands/{definition/dump-setup.d.ts → auth.d.ts} +0 -0
- /package/lib/cli/commands/{definition.d.ts → clone.d.ts} +0 -0
- /package/lib/cli/commands/{definition/validate.d.ts → groups.d.ts} +0 -0
- /package/lib/cli/commands/{delete-cache.d.ts → repos.d.ts} +0 -0
- /package/lib/cli/commands/{getting-started.d.ts → topics.d.ts} +0 -0
package/lib/cals-cli.mjs
CHANGED
|
@@ -1,1046 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as process
|
|
2
|
+
import * as process from 'node:process';
|
|
3
3
|
import process__default from 'node:process';
|
|
4
|
-
import semver from 'semver';
|
|
5
4
|
import yargs from 'yargs';
|
|
6
5
|
import { hideBin } from 'yargs/helpers';
|
|
6
|
+
import keytar from 'keytar';
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import chalk from 'chalk';
|
|
7
9
|
import fs from 'node:fs';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import cachedir from 'cachedir';
|
|
11
13
|
import { Buffer } from 'node:buffer';
|
|
12
14
|
import { performance } from 'node:perf_hooks';
|
|
13
15
|
import { Octokit } from '@octokit/rest';
|
|
14
|
-
import fetch from 'node-fetch';
|
|
15
16
|
import pLimit from 'p-limit';
|
|
16
|
-
import keytar from 'keytar';
|
|
17
|
-
import { deprecate } from 'node:util';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import https from 'node:https';
|
|
20
|
-
import os from 'node:os';
|
|
21
|
-
import cachedir from 'cachedir';
|
|
22
|
-
import readline from 'node:readline';
|
|
23
|
-
import chalk from 'chalk';
|
|
24
|
-
import { sprintf } from 'sprintf-js';
|
|
25
|
-
import { read } from 'read';
|
|
26
17
|
import { findUp } from 'find-up';
|
|
18
|
+
import yaml from 'js-yaml';
|
|
19
|
+
import AJV from 'ajv';
|
|
27
20
|
import { execa } from 'execa';
|
|
28
21
|
|
|
29
|
-
var version = "3.
|
|
22
|
+
var version = "3.15.0";
|
|
30
23
|
var engines = {
|
|
31
|
-
node: ">=22.14.0"
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function groupBy(array, iteratee) {
|
|
35
|
-
return array.reduce((result, item) => {
|
|
36
|
-
const key = iteratee(item);
|
|
37
|
-
if (!result[key]) {
|
|
38
|
-
result[key] = [];
|
|
39
|
-
}
|
|
40
|
-
result[key].push(item);
|
|
41
|
-
return result;
|
|
42
|
-
}, {});
|
|
43
|
-
}
|
|
44
|
-
function uniq(array) {
|
|
45
|
-
return Array.from(new Set(array));
|
|
46
|
-
}
|
|
47
|
-
function sortBy(arr, getKey) {
|
|
48
|
-
return [...arr].sort((a, b) => getKey(a).localeCompare(getKey(b)));
|
|
49
|
-
}
|
|
50
|
-
function sumBy(array, iteratee) {
|
|
51
|
-
return array.reduce((sum, item) => sum + iteratee(item), 0);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
var type = "object";
|
|
55
|
-
var properties = {
|
|
56
|
-
snyk: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
accountId: {
|
|
60
|
-
type: "string"
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
required: [
|
|
64
|
-
"accountId"
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
github: {
|
|
68
|
-
type: "object",
|
|
69
|
-
properties: {
|
|
70
|
-
users: {
|
|
71
|
-
type: "array",
|
|
72
|
-
items: {
|
|
73
|
-
$ref: "#/definitions/User"
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
teams: {
|
|
77
|
-
type: "array",
|
|
78
|
-
items: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
organization: {
|
|
82
|
-
type: "string"
|
|
83
|
-
},
|
|
84
|
-
teams: {
|
|
85
|
-
type: "array",
|
|
86
|
-
items: {
|
|
87
|
-
$ref: "#/definitions/Team"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
required: [
|
|
92
|
-
"organization",
|
|
93
|
-
"teams"
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
required: [
|
|
99
|
-
"teams",
|
|
100
|
-
"users"
|
|
101
|
-
]
|
|
102
|
-
},
|
|
103
|
-
projects: {
|
|
104
|
-
type: "array",
|
|
105
|
-
items: {
|
|
106
|
-
$ref: "#/definitions/Project"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
var required = [
|
|
111
|
-
"github",
|
|
112
|
-
"projects"
|
|
113
|
-
];
|
|
114
|
-
var definitions = {
|
|
115
|
-
User: {
|
|
116
|
-
anyOf: [
|
|
117
|
-
{
|
|
118
|
-
$ref: "#/definitions/UserBot"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
$ref: "#/definitions/UserEmployee"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
$ref: "#/definitions/UserExternal"
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
},
|
|
128
|
-
UserBot: {
|
|
129
|
-
type: "object",
|
|
130
|
-
properties: {
|
|
131
|
-
type: {
|
|
132
|
-
type: "string",
|
|
133
|
-
"const": "bot"
|
|
134
|
-
},
|
|
135
|
-
login: {
|
|
136
|
-
type: "string"
|
|
137
|
-
},
|
|
138
|
-
name: {
|
|
139
|
-
type: "string"
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
required: [
|
|
143
|
-
"login",
|
|
144
|
-
"name",
|
|
145
|
-
"type"
|
|
146
|
-
]
|
|
147
|
-
},
|
|
148
|
-
UserEmployee: {
|
|
149
|
-
type: "object",
|
|
150
|
-
properties: {
|
|
151
|
-
type: {
|
|
152
|
-
type: "string",
|
|
153
|
-
"const": "employee"
|
|
154
|
-
},
|
|
155
|
-
login: {
|
|
156
|
-
type: "string"
|
|
157
|
-
},
|
|
158
|
-
capraUsername: {
|
|
159
|
-
type: "string"
|
|
160
|
-
},
|
|
161
|
-
name: {
|
|
162
|
-
type: "string"
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
required: [
|
|
166
|
-
"capraUsername",
|
|
167
|
-
"login",
|
|
168
|
-
"name",
|
|
169
|
-
"type"
|
|
170
|
-
]
|
|
171
|
-
},
|
|
172
|
-
UserExternal: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
175
|
-
type: {
|
|
176
|
-
type: "string",
|
|
177
|
-
"const": "external"
|
|
178
|
-
},
|
|
179
|
-
login: {
|
|
180
|
-
type: "string"
|
|
181
|
-
},
|
|
182
|
-
name: {
|
|
183
|
-
type: "string"
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
required: [
|
|
187
|
-
"login",
|
|
188
|
-
"name",
|
|
189
|
-
"type"
|
|
190
|
-
]
|
|
191
|
-
},
|
|
192
|
-
Team: {
|
|
193
|
-
type: "object",
|
|
194
|
-
properties: {
|
|
195
|
-
name: {
|
|
196
|
-
type: "string"
|
|
197
|
-
},
|
|
198
|
-
members: {
|
|
199
|
-
type: "array",
|
|
200
|
-
items: {
|
|
201
|
-
type: "string"
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
required: [
|
|
206
|
-
"members",
|
|
207
|
-
"name"
|
|
208
|
-
]
|
|
209
|
-
},
|
|
210
|
-
Project: {
|
|
211
|
-
type: "object",
|
|
212
|
-
properties: {
|
|
213
|
-
name: {
|
|
214
|
-
type: "string"
|
|
215
|
-
},
|
|
216
|
-
github: {
|
|
217
|
-
type: "array",
|
|
218
|
-
items: {
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {
|
|
221
|
-
organization: {
|
|
222
|
-
type: "string"
|
|
223
|
-
},
|
|
224
|
-
repos: {
|
|
225
|
-
type: "array",
|
|
226
|
-
items: {
|
|
227
|
-
$ref: "#/definitions/DefinitionRepo"
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
teams: {
|
|
231
|
-
type: "array",
|
|
232
|
-
items: {
|
|
233
|
-
$ref: "#/definitions/RepoTeam"
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
required: [
|
|
238
|
-
"organization"
|
|
239
|
-
]
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
tags: {
|
|
243
|
-
type: "array",
|
|
244
|
-
items: {
|
|
245
|
-
type: "string"
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
responsible: {
|
|
249
|
-
description: "Some external-defined entity being responsible for the project.",
|
|
250
|
-
type: "string"
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
required: [
|
|
254
|
-
"github",
|
|
255
|
-
"name"
|
|
256
|
-
]
|
|
257
|
-
},
|
|
258
|
-
DefinitionRepo: {
|
|
259
|
-
type: "object",
|
|
260
|
-
properties: {
|
|
261
|
-
name: {
|
|
262
|
-
type: "string"
|
|
263
|
-
},
|
|
264
|
-
previousNames: {
|
|
265
|
-
type: "array",
|
|
266
|
-
items: {
|
|
267
|
-
$ref: "#/definitions/DefinitionRepoPreviousName"
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
archived: {
|
|
271
|
-
type: "boolean"
|
|
272
|
-
},
|
|
273
|
-
issues: {
|
|
274
|
-
type: "boolean"
|
|
275
|
-
},
|
|
276
|
-
wiki: {
|
|
277
|
-
type: "boolean"
|
|
278
|
-
},
|
|
279
|
-
teams: {
|
|
280
|
-
type: "array",
|
|
281
|
-
items: {
|
|
282
|
-
$ref: "#/definitions/RepoTeam"
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
snyk: {
|
|
286
|
-
type: "boolean"
|
|
287
|
-
},
|
|
288
|
-
"public": {
|
|
289
|
-
type: "boolean"
|
|
290
|
-
},
|
|
291
|
-
responsible: {
|
|
292
|
-
description: "Some external-defined entity being responsible for the repository.\n\nWill override the project-defined responsible.",
|
|
293
|
-
type: "string"
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
required: [
|
|
297
|
-
"name"
|
|
298
|
-
]
|
|
299
|
-
},
|
|
300
|
-
DefinitionRepoPreviousName: {
|
|
301
|
-
type: "object",
|
|
302
|
-
properties: {
|
|
303
|
-
name: {
|
|
304
|
-
type: "string"
|
|
305
|
-
},
|
|
306
|
-
project: {
|
|
307
|
-
type: "string"
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
required: [
|
|
311
|
-
"name",
|
|
312
|
-
"project"
|
|
313
|
-
]
|
|
314
|
-
},
|
|
315
|
-
RepoTeam: {
|
|
316
|
-
type: "object",
|
|
317
|
-
properties: {
|
|
318
|
-
name: {
|
|
319
|
-
type: "string"
|
|
320
|
-
},
|
|
321
|
-
permission: {
|
|
322
|
-
$ref: "#/definitions/Permission"
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
required: [
|
|
326
|
-
"name",
|
|
327
|
-
"permission"
|
|
328
|
-
]
|
|
329
|
-
},
|
|
330
|
-
Permission: {
|
|
331
|
-
"enum": [
|
|
332
|
-
"admin",
|
|
333
|
-
"pull",
|
|
334
|
-
"push"
|
|
335
|
-
],
|
|
336
|
-
type: "string"
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
var $schema = "http://json-schema.org/draft-07/schema#";
|
|
340
|
-
var schema = {
|
|
341
|
-
type: type,
|
|
342
|
-
properties: properties,
|
|
343
|
-
required: required,
|
|
344
|
-
definitions: definitions,
|
|
345
|
-
$schema: $schema
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
function getTeamId(org, teamName) {
|
|
349
|
-
return `${org}/${teamName}`;
|
|
350
|
-
}
|
|
351
|
-
function getRepoId(orgName, repoName) {
|
|
352
|
-
return `${orgName}/${repoName}`;
|
|
353
|
-
}
|
|
354
|
-
function checkAgainstSchema(value) {
|
|
355
|
-
const ajv = new AJV({ allErrors: true });
|
|
356
|
-
const valid = ajv.validate(schema, value);
|
|
357
|
-
return valid
|
|
358
|
-
? { definition: value }
|
|
359
|
-
: { error: ajv.errorsText() ?? "Unknown error" };
|
|
360
|
-
}
|
|
361
|
-
function requireValidDefinition(definition) {
|
|
362
|
-
// Verify no duplicates in users and extract known logins.
|
|
363
|
-
const loginList = definition.github.users.reduce((acc, user) => {
|
|
364
|
-
if (acc.includes(user.login)) {
|
|
365
|
-
throw new Error(`Duplicate login: ${user.login}`);
|
|
366
|
-
}
|
|
367
|
-
return [...acc, user.login];
|
|
368
|
-
}, []);
|
|
369
|
-
// Verify no duplicates in teams and extract team names.
|
|
370
|
-
const teamIdList = definition.github.teams.reduce((acc, orgTeams) => {
|
|
371
|
-
return orgTeams.teams.reduce((acc1, team) => {
|
|
372
|
-
const id = getTeamId(orgTeams.organization, team.name);
|
|
373
|
-
if (acc1.includes(id)) {
|
|
374
|
-
throw new Error(`Duplicate team: ${id}`);
|
|
375
|
-
}
|
|
376
|
-
return [...acc1, id];
|
|
377
|
-
}, acc);
|
|
378
|
-
}, []);
|
|
379
|
-
// Verify team members exists as users.
|
|
380
|
-
definition.github.teams
|
|
381
|
-
.flatMap((it) => it.teams)
|
|
382
|
-
.forEach((team) => {
|
|
383
|
-
team.members.forEach((login) => {
|
|
384
|
-
if (!loginList.includes(login)) {
|
|
385
|
-
throw new Error(`Team member ${login} in team ${team.name} is not registered in user list`);
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
// Verify no duplicates in project names.
|
|
390
|
-
definition.projects.reduce((acc, project) => {
|
|
391
|
-
if (acc.includes(project.name)) {
|
|
392
|
-
throw new Error(`Duplicate project: ${project.name}`);
|
|
393
|
-
}
|
|
394
|
-
return [...acc, project.name];
|
|
395
|
-
}, []);
|
|
396
|
-
definition.projects.forEach((project) => {
|
|
397
|
-
project.github.forEach((org) => {
|
|
398
|
-
(org.teams || []).forEach((team) => {
|
|
399
|
-
const id = getTeamId(org.organization, team.name);
|
|
400
|
-
if (!teamIdList.includes(id)) {
|
|
401
|
-
throw new Error(`Project team ${id} in project ${project.name} is not registered in team list`);
|
|
402
|
-
}
|
|
403
|
-
}) // Verify repo teams exists as teams.
|
|
404
|
-
;
|
|
405
|
-
(org.repos || []).forEach((repo) => {
|
|
406
|
-
(repo.teams || []).forEach((team) => {
|
|
407
|
-
const id = getTeamId(org.organization, team.name);
|
|
408
|
-
if (!teamIdList.includes(id)) {
|
|
409
|
-
throw new Error(`Repo team ${id} for repo ${repo.name} in project ${project.name} is not registered in team list`);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
// Verify no duplicates in repos.
|
|
416
|
-
definition.projects
|
|
417
|
-
.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => getRepoId(org.organization, repo.name))))
|
|
418
|
-
.reduce((acc, repoName) => {
|
|
419
|
-
if (acc.includes(repoName)) {
|
|
420
|
-
throw new Error(`Duplicate repo: ${repoName}`);
|
|
421
|
-
}
|
|
422
|
-
return [...acc, repoName];
|
|
423
|
-
}, []);
|
|
424
|
-
}
|
|
425
|
-
class DefinitionFile {
|
|
426
|
-
path;
|
|
427
|
-
constructor(path) {
|
|
428
|
-
this.path = path;
|
|
429
|
-
}
|
|
430
|
-
async getContents() {
|
|
431
|
-
return new Promise((resolve, reject) => fs.readFile(this.path, "utf-8", (err, data) => {
|
|
432
|
-
if (err)
|
|
433
|
-
reject(err);
|
|
434
|
-
else
|
|
435
|
-
resolve(data);
|
|
436
|
-
}));
|
|
437
|
-
}
|
|
438
|
-
async getDefinition() {
|
|
439
|
-
return parseDefinition(await this.getContents());
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function parseDefinition(value) {
|
|
443
|
-
const result = checkAgainstSchema(yaml.load(value));
|
|
444
|
-
if ("error" in result) {
|
|
445
|
-
throw new Error(`Definition content invalid: ${result.error}`);
|
|
446
|
-
}
|
|
447
|
-
requireValidDefinition(result.definition);
|
|
448
|
-
return result.definition;
|
|
449
|
-
}
|
|
450
|
-
function getRepos(definition) {
|
|
451
|
-
return definition.projects.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => ({
|
|
452
|
-
id: getRepoId(org.organization, repo.name),
|
|
453
|
-
orgName: org.organization,
|
|
454
|
-
project,
|
|
455
|
-
repo,
|
|
456
|
-
}))));
|
|
457
|
-
}
|
|
458
|
-
function getGitHubOrgs(definition) {
|
|
459
|
-
const githubOrganizations = definition.projects.flatMap((project) => project.github.map((it) => it.organization));
|
|
460
|
-
return uniq(githubOrganizations);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
class GitHubTokenCliProvider {
|
|
464
|
-
keyringService = "cals";
|
|
465
|
-
keyringAccount = "github-token";
|
|
466
|
-
async getToken() {
|
|
467
|
-
if (process__default.env.CALS_GITHUB_TOKEN) {
|
|
468
|
-
return process__default.env.CALS_GITHUB_TOKEN;
|
|
469
|
-
}
|
|
470
|
-
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
471
|
-
if (result == null) {
|
|
472
|
-
process__default.stderr.write("No token found. Register using `cals github set-token`\n");
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
return result;
|
|
476
|
-
}
|
|
477
|
-
async markInvalid() {
|
|
478
|
-
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
479
|
-
}
|
|
480
|
-
async setToken(value) {
|
|
481
|
-
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function getGroup(repo) {
|
|
486
|
-
const projectTopics = [];
|
|
487
|
-
let isInfra = false;
|
|
488
|
-
repo.repositoryTopics.edges.forEach((edge) => {
|
|
489
|
-
const name = edge.node.topic.name;
|
|
490
|
-
if (name.startsWith("customer-")) {
|
|
491
|
-
projectTopics.push(name.substring(9));
|
|
492
|
-
}
|
|
493
|
-
if (name.startsWith("project-")) {
|
|
494
|
-
projectTopics.push(name.substring(8));
|
|
495
|
-
}
|
|
496
|
-
if (name === "infrastructure") {
|
|
497
|
-
isInfra = true;
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
if (projectTopics.length > 1) {
|
|
501
|
-
console.warn(`Repo ${repo.name} has multiple project groups: ${projectTopics.join(", ")}. Picking first`);
|
|
502
|
-
}
|
|
503
|
-
if (projectTopics.length > 0) {
|
|
504
|
-
return projectTopics[0];
|
|
505
|
-
}
|
|
506
|
-
if (isInfra) {
|
|
507
|
-
return "infrastructure";
|
|
508
|
-
}
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
function ifnull(a, other) {
|
|
512
|
-
return a === null ? other : a;
|
|
513
|
-
}
|
|
514
|
-
function getGroupedRepos(repos) {
|
|
515
|
-
return Object.values(repos.reduce((acc, repo) => {
|
|
516
|
-
const group = ifnull(getGroup(repo), "(unknown)");
|
|
517
|
-
const value = acc[group] || { name: group, items: [] };
|
|
518
|
-
return {
|
|
519
|
-
...acc,
|
|
520
|
-
[group]: {
|
|
521
|
-
...value,
|
|
522
|
-
items: [...value.items, repo],
|
|
523
|
-
},
|
|
524
|
-
};
|
|
525
|
-
}, {})).sort((a, b) => a.name.localeCompare(b.name));
|
|
526
|
-
}
|
|
527
|
-
function includesTopic(repo, topic) {
|
|
528
|
-
return repo.repositoryTopics.edges.some((it) => it.node.topic.name === topic);
|
|
529
|
-
}
|
|
530
|
-
async function undefinedForNotFound(value) {
|
|
531
|
-
try {
|
|
532
|
-
return await value;
|
|
533
|
-
}
|
|
534
|
-
catch (e) {
|
|
535
|
-
if (e.name === "HttpError" && e.status === 404) {
|
|
536
|
-
return undefined;
|
|
537
|
-
}
|
|
538
|
-
throw e;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
class GitHubService {
|
|
543
|
-
config;
|
|
544
|
-
octokit;
|
|
545
|
-
cache;
|
|
546
|
-
tokenProvider;
|
|
547
|
-
semaphore;
|
|
548
|
-
constructor(props) {
|
|
549
|
-
this.config = props.config;
|
|
550
|
-
this.octokit = props.octokit;
|
|
551
|
-
this.cache = props.cache;
|
|
552
|
-
this.tokenProvider = props.tokenProvider;
|
|
553
|
-
// Control concurrency to GitHub API at service level so we
|
|
554
|
-
// can maximize concurrency all other places.
|
|
555
|
-
this.semaphore = pLimit(6);
|
|
556
|
-
this.octokit.hook.wrap("request", async (request, options) => {
|
|
557
|
-
this._requestCount++;
|
|
558
|
-
if (options.method !== "GET") {
|
|
559
|
-
return this.semaphore(() => request(options));
|
|
560
|
-
}
|
|
561
|
-
// Try to cache ETag for GET requests to save on rate limiting.
|
|
562
|
-
// Hits on ETag does not count towards rate limiting.
|
|
563
|
-
const rest = {
|
|
564
|
-
...options,
|
|
565
|
-
};
|
|
566
|
-
delete rest.method;
|
|
567
|
-
delete rest.baseUrl;
|
|
568
|
-
delete rest.headers;
|
|
569
|
-
delete rest.mediaType;
|
|
570
|
-
delete rest.request;
|
|
571
|
-
// Build a key that is used to identify this request.
|
|
572
|
-
const key = Buffer.from(JSON.stringify(rest)).toString("base64");
|
|
573
|
-
const cacheItem = this.cache.retrieveJson(key);
|
|
574
|
-
if (cacheItem !== undefined) {
|
|
575
|
-
// Copying doesn't work, seems we need to mutate this.
|
|
576
|
-
options.headers["If-None-Match"] = cacheItem.data.etag;
|
|
577
|
-
}
|
|
578
|
-
const getResponse = async (allowRetry = true) => {
|
|
579
|
-
try {
|
|
580
|
-
return await request(options);
|
|
581
|
-
}
|
|
582
|
-
catch (e) {
|
|
583
|
-
// Handle no change in ETag.
|
|
584
|
-
if (e.status === 304) {
|
|
585
|
-
return undefined;
|
|
586
|
-
}
|
|
587
|
-
// GitHub seems to throw a lot of 502 errors.
|
|
588
|
-
// Let's give it a few seconds and retry one time.
|
|
589
|
-
if (e.status === 502 && allowRetry) {
|
|
590
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
591
|
-
return await getResponse(false);
|
|
592
|
-
}
|
|
593
|
-
throw e;
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
const response = await this.semaphore(async () => {
|
|
597
|
-
return getResponse();
|
|
598
|
-
});
|
|
599
|
-
if (response === undefined) {
|
|
600
|
-
// Undefined is returned for cached data.
|
|
601
|
-
if (cacheItem === undefined) {
|
|
602
|
-
throw new Error("Missing expected cache item");
|
|
603
|
-
}
|
|
604
|
-
// Use previous value.
|
|
605
|
-
return cacheItem.data.data;
|
|
606
|
-
}
|
|
607
|
-
// New value. Store Etag.
|
|
608
|
-
if (response.headers.etag) {
|
|
609
|
-
this.cache.storeJson(key, {
|
|
610
|
-
etag: response.headers.etag,
|
|
611
|
-
data: response,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
return response;
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
_requestCount = 0;
|
|
618
|
-
get requestCount() {
|
|
619
|
-
return this._requestCount;
|
|
620
|
-
}
|
|
621
|
-
async runGraphqlQuery(query) {
|
|
622
|
-
const token = await this.tokenProvider.getToken();
|
|
623
|
-
if (token === undefined) {
|
|
624
|
-
throw new Error("Missing token for GitHub");
|
|
625
|
-
}
|
|
626
|
-
const url = "https://api.github.com/graphql";
|
|
627
|
-
const headers = {
|
|
628
|
-
Authorization: `Bearer ${token}`,
|
|
629
|
-
};
|
|
630
|
-
let requestDuration = -1;
|
|
631
|
-
const response = await this.semaphore(() => {
|
|
632
|
-
const requestStart = performance.now();
|
|
633
|
-
const result = fetch(url, {
|
|
634
|
-
method: "POST",
|
|
635
|
-
headers,
|
|
636
|
-
body: JSON.stringify({ query }),
|
|
637
|
-
agent: this.config.agent,
|
|
638
|
-
});
|
|
639
|
-
requestDuration = performance.now() - requestStart;
|
|
640
|
-
return result;
|
|
641
|
-
});
|
|
642
|
-
if (response.status === 401) {
|
|
643
|
-
process$1.stderr.write("Unauthorized\n");
|
|
644
|
-
await this.tokenProvider.markInvalid();
|
|
645
|
-
}
|
|
646
|
-
// If you get 502 after 10s, it is a timeout.
|
|
647
|
-
if (response.status === 502) {
|
|
648
|
-
throw new Error(`Response from Github likely timed out (10s max) after elapsed ${requestDuration}ms with status ${response.status}: ${await response.text()}`);
|
|
649
|
-
}
|
|
650
|
-
if (!response.ok) {
|
|
651
|
-
throw new Error(`Response from GitHub not OK (${response.status}): ${await response.text()}`);
|
|
652
|
-
}
|
|
653
|
-
const json = (await response.json());
|
|
654
|
-
if (json.errors) {
|
|
655
|
-
throw new Error(`Error from GitHub GraphQL API: ${JSON.stringify(json.errors)}`);
|
|
656
|
-
}
|
|
657
|
-
if (json.data == null) {
|
|
658
|
-
throw new Error("No data received from GitHub GraphQL API (unknown reason)");
|
|
659
|
-
}
|
|
660
|
-
return json.data;
|
|
661
|
-
}
|
|
662
|
-
async getOrgRepoList({ org }) {
|
|
663
|
-
const getQuery = (after) => `{
|
|
664
|
-
organization(login: "${org}") {
|
|
665
|
-
repositories(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
666
|
-
totalCount
|
|
667
|
-
pageInfo {
|
|
668
|
-
hasNextPage
|
|
669
|
-
endCursor
|
|
670
|
-
}
|
|
671
|
-
nodes {
|
|
672
|
-
name
|
|
673
|
-
owner {
|
|
674
|
-
login
|
|
675
|
-
}
|
|
676
|
-
defaultBranchRef {
|
|
677
|
-
name
|
|
678
|
-
}
|
|
679
|
-
createdAt
|
|
680
|
-
updatedAt
|
|
681
|
-
isArchived
|
|
682
|
-
sshUrl
|
|
683
|
-
repositoryTopics(first: 100) {
|
|
684
|
-
edges {
|
|
685
|
-
node {
|
|
686
|
-
topic {
|
|
687
|
-
name
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}`;
|
|
696
|
-
return this.cache.json(`repos-${org}`, async () => {
|
|
697
|
-
const repos = [];
|
|
698
|
-
let after = null;
|
|
699
|
-
while (true) {
|
|
700
|
-
const query = getQuery(after);
|
|
701
|
-
const res = await this.runGraphqlQuery(query);
|
|
702
|
-
if (res.organization == null) {
|
|
703
|
-
throw new Error("Missing organization");
|
|
704
|
-
}
|
|
705
|
-
if (res.organization.repositories.nodes == null) {
|
|
706
|
-
throw new Error("Missing organization nodes");
|
|
707
|
-
}
|
|
708
|
-
repos.push(...res.organization.repositories.nodes);
|
|
709
|
-
if (!res.organization.repositories.pageInfo.hasNextPage) {
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
after = res.organization.repositories.pageInfo.endCursor;
|
|
713
|
-
}
|
|
714
|
-
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
async getOrgMembersList(org) {
|
|
718
|
-
const options = this.octokit.orgs.listMembers.endpoint.merge({
|
|
719
|
-
org,
|
|
720
|
-
});
|
|
721
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
722
|
-
}
|
|
723
|
-
async getOrgMembersInvitedList(org) {
|
|
724
|
-
const options = this.octokit.orgs.listPendingInvitations.endpoint.merge({
|
|
725
|
-
org,
|
|
726
|
-
});
|
|
727
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
728
|
-
}
|
|
729
|
-
async getOrgMembersListIncludingInvited(org) {
|
|
730
|
-
return [
|
|
731
|
-
...(await this.getOrgMembersList(org)).map((it) => ({
|
|
732
|
-
type: "member",
|
|
733
|
-
login: it.login,
|
|
734
|
-
data: it,
|
|
735
|
-
})),
|
|
736
|
-
...(await this.getOrgMembersInvitedList(org)).map((it) => ({
|
|
737
|
-
type: "invited",
|
|
738
|
-
// TODO: Fix ?? case properly
|
|
739
|
-
login: it.login ?? "invalid",
|
|
740
|
-
data: it,
|
|
741
|
-
})),
|
|
742
|
-
];
|
|
743
|
-
}
|
|
744
|
-
async getRepository(owner, repo) {
|
|
745
|
-
return this.cache.json(`get-repository-${owner}-${repo}`, async () => {
|
|
746
|
-
const response = await undefinedForNotFound(this.octokit.repos.get({
|
|
747
|
-
owner,
|
|
748
|
-
repo,
|
|
749
|
-
}));
|
|
750
|
-
return response === undefined ? undefined : response.data;
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
async getRepositoryTeamsList(repo) {
|
|
754
|
-
return this.cache.json(`repository-teams-list-${repo.id}`, async () => {
|
|
755
|
-
const options = this.octokit.repos.listTeams.endpoint.merge({
|
|
756
|
-
owner: repo.owner.login,
|
|
757
|
-
repo: repo.name,
|
|
758
|
-
});
|
|
759
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
async getRepositoryHooks(owner, repo) {
|
|
763
|
-
return this.cache.json(`repository-hooks-${owner}-${repo}`, async () => {
|
|
764
|
-
const options = this.octokit.repos.listWebhooks.endpoint.merge({
|
|
765
|
-
owner,
|
|
766
|
-
repo,
|
|
767
|
-
});
|
|
768
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
async getOrg(org) {
|
|
772
|
-
const orgResponse = await this.octokit.orgs.get({
|
|
773
|
-
org,
|
|
774
|
-
});
|
|
775
|
-
return orgResponse.data;
|
|
776
|
-
}
|
|
777
|
-
async getTeamList(org) {
|
|
778
|
-
return this.cache.json(`team-list-${org.login}`, async () => {
|
|
779
|
-
const options = this.octokit.teams.list.endpoint.merge({
|
|
780
|
-
org: org.login,
|
|
781
|
-
});
|
|
782
|
-
return (await this.octokit.paginate(options));
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
async getTeamMemberList(org, team) {
|
|
786
|
-
return this.cache.json(`team-member-list-${team.id}`, async () => {
|
|
787
|
-
const options = this.octokit.teams.listMembersInOrg.endpoint.merge({
|
|
788
|
-
org: org.login,
|
|
789
|
-
team_slug: team.slug,
|
|
790
|
-
});
|
|
791
|
-
return (await this.octokit.paginate(options));
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
async getTeamMemberInvitedList(org, team) {
|
|
795
|
-
return this.cache.json(`team-member-invited-list-${team.id}`, async () => {
|
|
796
|
-
const options = this.octokit.teams.listPendingInvitationsInOrg.endpoint.merge({
|
|
797
|
-
org: org.login,
|
|
798
|
-
team_slug: team.slug,
|
|
799
|
-
});
|
|
800
|
-
return (await this.octokit.paginate(options));
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
async getTeamMemberListIncludingInvited(org, team) {
|
|
804
|
-
return [
|
|
805
|
-
...(await this.getTeamMemberList(org, team)).map((it) => ({
|
|
806
|
-
type: "member",
|
|
807
|
-
login: it.login,
|
|
808
|
-
data: it,
|
|
809
|
-
})),
|
|
810
|
-
...(await this.getTeamMemberInvitedList(org, team)).map((it) => ({
|
|
811
|
-
type: "invited",
|
|
812
|
-
// TODO: Fix ?? case properly
|
|
813
|
-
login: it.login ?? "invalid",
|
|
814
|
-
data: it,
|
|
815
|
-
})),
|
|
816
|
-
];
|
|
817
|
-
}
|
|
818
|
-
async getSearchedPullRequestList(owner) {
|
|
819
|
-
// NOTE: Changes to this must by synced with SearchedPullRequestListQueryResult.
|
|
820
|
-
const getQuery = (after) => `{
|
|
821
|
-
search(
|
|
822
|
-
query: "is:open is:pr user:${owner} owner:${owner} archived:false",
|
|
823
|
-
type: ISSUE,
|
|
824
|
-
first: 50${after === null
|
|
825
|
-
? ""
|
|
826
|
-
: `,
|
|
827
|
-
after: "${after}"`}
|
|
828
|
-
) {
|
|
829
|
-
pageInfo {
|
|
830
|
-
hasNextPage
|
|
831
|
-
endCursor
|
|
832
|
-
}
|
|
833
|
-
edges {
|
|
834
|
-
node {
|
|
835
|
-
__typename
|
|
836
|
-
... on PullRequest {
|
|
837
|
-
number
|
|
838
|
-
baseRepository {
|
|
839
|
-
name
|
|
840
|
-
owner {
|
|
841
|
-
login
|
|
842
|
-
}
|
|
843
|
-
defaultBranchRef {
|
|
844
|
-
name
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
author {
|
|
848
|
-
login
|
|
849
|
-
}
|
|
850
|
-
title
|
|
851
|
-
commits(first: 3) {
|
|
852
|
-
nodes {
|
|
853
|
-
commit {
|
|
854
|
-
messageHeadline
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
createdAt
|
|
859
|
-
updatedAt
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}`;
|
|
865
|
-
const pulls = [];
|
|
866
|
-
let after = null;
|
|
867
|
-
while (true) {
|
|
868
|
-
const query = getQuery(after);
|
|
869
|
-
const res = await this.runGraphqlQuery(query);
|
|
870
|
-
pulls.push(...res.search.edges.map((it) => it.node));
|
|
871
|
-
if (!res.search.pageInfo.hasNextPage) {
|
|
872
|
-
break;
|
|
873
|
-
}
|
|
874
|
-
after = res.search.pageInfo.endCursor;
|
|
875
|
-
}
|
|
876
|
-
return pulls.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
877
|
-
}
|
|
878
|
-
async getHasVulnerabilityAlertsEnabled(owner, repo) {
|
|
879
|
-
try {
|
|
880
|
-
const response = await this.octokit.repos.checkVulnerabilityAlerts({
|
|
881
|
-
owner: owner,
|
|
882
|
-
repo: repo,
|
|
883
|
-
});
|
|
884
|
-
if (response.status !== 204) {
|
|
885
|
-
console.log(response);
|
|
886
|
-
throw new Error("Unknown response - see previous log line");
|
|
887
|
-
}
|
|
888
|
-
return true;
|
|
889
|
-
}
|
|
890
|
-
catch (e) {
|
|
891
|
-
if (e.status === 404) {
|
|
892
|
-
return false;
|
|
893
|
-
}
|
|
894
|
-
throw e;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
async enableVulnerabilityAlerts(owner, repo) {
|
|
898
|
-
await this.octokit.repos.enableVulnerabilityAlerts({
|
|
899
|
-
owner: owner,
|
|
900
|
-
repo: repo,
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Get the vulnerability alerts for a repository.
|
|
905
|
-
*/
|
|
906
|
-
async getVulnerabilityAlerts(owner, repo) {
|
|
907
|
-
// NOTE: Changes to this must by synced with VulnerabilityAlertsQueryResult.
|
|
908
|
-
const getQuery = (after) => `{
|
|
909
|
-
repository(owner: "${owner}", name: "${repo}") {
|
|
910
|
-
vulnerabilityAlerts(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
911
|
-
pageInfo {
|
|
912
|
-
hasNextPage
|
|
913
|
-
endCursor
|
|
914
|
-
}
|
|
915
|
-
edges {
|
|
916
|
-
node {
|
|
917
|
-
state
|
|
918
|
-
dismissReason
|
|
919
|
-
vulnerableManifestFilename
|
|
920
|
-
vulnerableManifestPath
|
|
921
|
-
vulnerableRequirements
|
|
922
|
-
securityAdvisory {
|
|
923
|
-
description
|
|
924
|
-
identifiers { type value }
|
|
925
|
-
references { url }
|
|
926
|
-
severity
|
|
927
|
-
}
|
|
928
|
-
securityVulnerability {
|
|
929
|
-
package { name ecosystem }
|
|
930
|
-
firstPatchedVersion { identifier }
|
|
931
|
-
vulnerableVersionRange
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}`;
|
|
938
|
-
return this.cache.json(`vulnerability-alerts-${owner}-${repo}`, async () => {
|
|
939
|
-
const result = [];
|
|
940
|
-
let after = null;
|
|
941
|
-
while (true) {
|
|
942
|
-
const query = getQuery(after);
|
|
943
|
-
const res = await this.runGraphqlQuery(query);
|
|
944
|
-
result.push(...(res.repository?.vulnerabilityAlerts.edges?.map((it) => it.node) ?? []));
|
|
945
|
-
if (!res.repository?.vulnerabilityAlerts.pageInfo.hasNextPage) {
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
after = res.repository?.vulnerabilityAlerts.pageInfo.endCursor;
|
|
949
|
-
}
|
|
950
|
-
return result;
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Get the Renovate Dependency Dashboard issue.
|
|
955
|
-
*/
|
|
956
|
-
async getRenovateDependencyDashboardIssue(owner, repo) {
|
|
957
|
-
// NOTE: Changes to this must by synced with RenovateDependencyDashboardIssueQueryResult.
|
|
958
|
-
const getQuery = (after) => `{
|
|
959
|
-
repository(owner: "${owner}", name: "${repo}") {
|
|
960
|
-
issues(
|
|
961
|
-
orderBy: {field: UPDATED_AT, direction: DESC},
|
|
962
|
-
filterBy: {createdBy: "renovate[bot]"},
|
|
963
|
-
states: [OPEN],
|
|
964
|
-
first: 100${after === null ? "" : `, after: "${after}"`}
|
|
965
|
-
) {
|
|
966
|
-
pageInfo {
|
|
967
|
-
hasNextPage
|
|
968
|
-
endCursor
|
|
969
|
-
}
|
|
970
|
-
edges {
|
|
971
|
-
node {
|
|
972
|
-
number
|
|
973
|
-
state
|
|
974
|
-
title
|
|
975
|
-
body
|
|
976
|
-
userContentEdits(first: 5) {
|
|
977
|
-
nodes {
|
|
978
|
-
createdAt
|
|
979
|
-
editor {
|
|
980
|
-
login
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}`;
|
|
989
|
-
const issues = await this.cache.json(`renovate-bot-issues-${owner}-${repo}`, async () => {
|
|
990
|
-
const result = [];
|
|
991
|
-
let after = null;
|
|
992
|
-
while (true) {
|
|
993
|
-
const query = getQuery(after);
|
|
994
|
-
const res = await this.runGraphqlQuery(query);
|
|
995
|
-
const nodes = res.repository?.issues.edges?.map((it) => it.node) ?? [];
|
|
996
|
-
result.push(...nodes
|
|
997
|
-
.filter((it) => it.title === "Dependency Dashboard")
|
|
998
|
-
.map((it) => ({
|
|
999
|
-
number: it.number,
|
|
1000
|
-
body: it.body,
|
|
1001
|
-
lastUpdatedByRenovate: it.userContentEdits?.nodes?.filter((it) => it.editor?.login === "renovate")?.[0]?.createdAt ?? null,
|
|
1002
|
-
})));
|
|
1003
|
-
if (!res.repository?.issues.pageInfo.hasNextPage) {
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
after = res.repository?.issues.pageInfo.endCursor;
|
|
1007
|
-
}
|
|
1008
|
-
return result;
|
|
1009
|
-
});
|
|
1010
|
-
if (issues.length == 0) {
|
|
1011
|
-
return undefined;
|
|
1012
|
-
}
|
|
1013
|
-
return issues[0];
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
async function createOctokit(config, tokenProvider) {
|
|
1017
|
-
return new Octokit({
|
|
1018
|
-
auth: await tokenProvider.getToken(),
|
|
1019
|
-
request: {
|
|
1020
|
-
agent: config.agent,
|
|
1021
|
-
},
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
async function createGitHubService(props) {
|
|
1025
|
-
const tokenProvider = props.tokenProvider ?? new GitHubTokenCliProvider();
|
|
1026
|
-
return new GitHubService({
|
|
1027
|
-
config: props.config,
|
|
1028
|
-
octokit: await createOctokit(props.config, tokenProvider),
|
|
1029
|
-
cache: props.cache,
|
|
1030
|
-
tokenProvider,
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
24
|
+
node: ">=22.14.0"
|
|
25
|
+
};
|
|
1033
26
|
|
|
1034
|
-
class
|
|
27
|
+
class GitHubTokenCliProvider {
|
|
1035
28
|
keyringService = "cals";
|
|
1036
|
-
keyringAccount = "
|
|
29
|
+
keyringAccount = "github-token";
|
|
1037
30
|
async getToken() {
|
|
1038
|
-
if (process__default.env.
|
|
1039
|
-
return process__default.env.
|
|
31
|
+
if (process__default.env.CALS_GITHUB_TOKEN) {
|
|
32
|
+
return process__default.env.CALS_GITHUB_TOKEN;
|
|
1040
33
|
}
|
|
1041
34
|
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
1042
35
|
if (result == null) {
|
|
1043
|
-
process__default.stderr.write("No token found. Register using `cals
|
|
36
|
+
process__default.stderr.write("No token found. Register using `cals github set-token`\n");
|
|
1044
37
|
return undefined;
|
|
1045
38
|
}
|
|
1046
39
|
return result;
|
|
@@ -1053,124 +46,95 @@ class SnykTokenCliProvider {
|
|
|
1053
46
|
}
|
|
1054
47
|
}
|
|
1055
48
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
this.config = props.config;
|
|
1061
|
-
this.tokenProvider = props.tokenProvider;
|
|
1062
|
-
}
|
|
1063
|
-
async getProjects(definition) {
|
|
1064
|
-
const snykAccountId = definition.snyk?.accountId;
|
|
1065
|
-
if (snykAccountId === undefined) {
|
|
1066
|
-
return [];
|
|
1067
|
-
}
|
|
1068
|
-
return this.getProjectsByAccountId(snykAccountId);
|
|
1069
|
-
}
|
|
1070
|
-
async getProjectsByAccountId(snykAccountId,
|
|
1071
|
-
/**
|
|
1072
|
-
* The slug name of a Snyk organization.
|
|
1073
|
-
*
|
|
1074
|
-
* NOTE: This is only used to construct the browsable URL for a given project, and is not being used
|
|
1075
|
-
* in API calls to Snyk.
|
|
1076
|
-
*
|
|
1077
|
-
* @default - the slug corresponding to Lifligs Snyk organization ("it").
|
|
1078
|
-
*/
|
|
1079
|
-
snykOrgSlugId) {
|
|
1080
|
-
const token = await this.tokenProvider.getToken();
|
|
1081
|
-
if (token === undefined) {
|
|
1082
|
-
throw new Error("Missing token for Snyk");
|
|
1083
|
-
}
|
|
1084
|
-
let backportedProjects = [];
|
|
1085
|
-
const snykRestApiVersion = "2025-11-05";
|
|
1086
|
-
let nextUrl = `/rest/orgs/${encodeURIComponent(snykAccountId)}/projects?version=${snykRestApiVersion}&meta.latest_dependency_total=true&meta.latest_issue_counts=true&limit=100`;
|
|
1087
|
-
/* The Snyk REST API only allows us to retrieve 100 projects at a time.
|
|
1088
|
-
* The "links.next" value in the response gives us a pointer to the next 100 results.
|
|
1089
|
-
* We continue calling the Snyk API and retrieving more projects until links.next is null
|
|
1090
|
-
* */
|
|
1091
|
-
while (nextUrl) {
|
|
1092
|
-
const response = await fetch(`https://api.snyk.io${nextUrl}`, {
|
|
1093
|
-
method: "GET",
|
|
1094
|
-
headers: {
|
|
1095
|
-
Accept: "application/json",
|
|
1096
|
-
Authorization: `token ${token}`,
|
|
1097
|
-
},
|
|
1098
|
-
agent: this.config.agent,
|
|
1099
|
-
});
|
|
1100
|
-
if (response.status === 401) {
|
|
1101
|
-
process__default.stderr.write("Unauthorized - removing token\n");
|
|
1102
|
-
await this.tokenProvider.markInvalid();
|
|
1103
|
-
}
|
|
1104
|
-
if (!response.ok) {
|
|
1105
|
-
throw new Error(`Response from Snyk not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
1106
|
-
}
|
|
1107
|
-
// Check if the Sunset header is present in the response
|
|
1108
|
-
const sunsetHeader = response.headers.get("Sunset") || response.headers.get("sunset");
|
|
1109
|
-
if (sunsetHeader) {
|
|
1110
|
-
console.warn(`Snyk endpoint with version ${snykRestApiVersion} has been marked as deprecated with deprecation date ${sunsetHeader}`);
|
|
1111
|
-
}
|
|
1112
|
-
const jsonResponse = (await response.json());
|
|
1113
|
-
/* We transform the data to a standard format that we used for data from Snyk API v1 in order for
|
|
1114
|
-
the data to be backover compatible with existing consuments */
|
|
1115
|
-
backportedProjects = [
|
|
1116
|
-
...backportedProjects,
|
|
1117
|
-
...jsonResponse.data.map((project) => {
|
|
1118
|
-
return {
|
|
1119
|
-
id: project.id,
|
|
1120
|
-
name: project.attributes.name,
|
|
1121
|
-
type: project.attributes.type,
|
|
1122
|
-
created: project.attributes.created,
|
|
1123
|
-
origin: project.attributes.origin,
|
|
1124
|
-
testFrequency: project.attributes.settings.recurring_tests.frequency,
|
|
1125
|
-
isMonitored: project.attributes.status === "active",
|
|
1126
|
-
totalDependencies: project.meta.latest_dependency_total.total,
|
|
1127
|
-
issueCountsBySeverity: project.meta.latest_issue_counts,
|
|
1128
|
-
lastTestedDate: project.meta.latest_dependency_total.updated_at,
|
|
1129
|
-
browseUrl: `https://app.snyk.io/org/${snykOrgSlugId ?? "it"}/project/${project.id}`,
|
|
1130
|
-
};
|
|
1131
|
-
}),
|
|
1132
|
-
];
|
|
1133
|
-
/* Update nextUrl with pointer to the next page of results based
|
|
1134
|
-
* on the "links.next" field in the JSON response */
|
|
1135
|
-
nextUrl = jsonResponse.links.next;
|
|
1136
|
-
}
|
|
1137
|
-
return backportedProjects;
|
|
1138
|
-
}
|
|
49
|
+
const CLEAR_WHOLE_LINE = 0;
|
|
50
|
+
function clearLine(stdout) {
|
|
51
|
+
readline.clearLine(stdout, CLEAR_WHOLE_LINE);
|
|
52
|
+
readline.cursorTo(stdout, 0);
|
|
1139
53
|
}
|
|
1140
|
-
function
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
54
|
+
async function readInput(options) {
|
|
55
|
+
process__default.stdout.write(options.prompt);
|
|
56
|
+
// For silent mode, read character by character with raw mode to hide input
|
|
57
|
+
if (options.silent && process__default.stdin.isTTY) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let input = "";
|
|
60
|
+
process__default.stdin.setRawMode(true);
|
|
61
|
+
process__default.stdin.resume();
|
|
62
|
+
process__default.stdin.setEncoding("utf8");
|
|
63
|
+
const timer = options.timeout
|
|
64
|
+
? setTimeout(() => {
|
|
65
|
+
cleanup();
|
|
66
|
+
reject(new Error("Input timed out"));
|
|
67
|
+
}, options.timeout)
|
|
68
|
+
: null;
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
process__default.stdin.setRawMode(false);
|
|
71
|
+
process__default.stdin.pause();
|
|
72
|
+
process__default.stdin.removeListener("data", onData);
|
|
73
|
+
if (timer)
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
};
|
|
76
|
+
const onData = (char) => {
|
|
77
|
+
if (char === "\r" || char === "\n") {
|
|
78
|
+
cleanup();
|
|
79
|
+
process__default.stdout.write("\n");
|
|
80
|
+
resolve(input);
|
|
81
|
+
}
|
|
82
|
+
else if (char === "\u0003") {
|
|
83
|
+
// Ctrl+C
|
|
84
|
+
cleanup();
|
|
85
|
+
process__default.exit(1);
|
|
86
|
+
}
|
|
87
|
+
else if (char === "\u007F" || char === "\b") {
|
|
88
|
+
// Backspace
|
|
89
|
+
input = input.slice(0, -1);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
input += char;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
process__default.stdin.on("data", onData);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Normal (non-silent) mode
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input: process__default.stdin,
|
|
101
|
+
output: process__default.stdout,
|
|
102
|
+
});
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const timer = options.timeout
|
|
105
|
+
? setTimeout(() => {
|
|
106
|
+
rl.close();
|
|
107
|
+
reject(new Error("Input timed out"));
|
|
108
|
+
}, options.timeout)
|
|
109
|
+
: null;
|
|
110
|
+
rl.question("", (answer) => {
|
|
111
|
+
if (timer)
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
rl.close();
|
|
114
|
+
resolve(answer);
|
|
115
|
+
});
|
|
1144
116
|
});
|
|
1145
117
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
return {
|
|
1154
|
-
owner: match[1],
|
|
1155
|
-
name: match[2],
|
|
1156
|
-
};
|
|
118
|
+
class Reporter {
|
|
119
|
+
stdout = process__default.stdout;
|
|
120
|
+
stderr = process__default.stderr;
|
|
121
|
+
format = chalk;
|
|
122
|
+
error(msg) {
|
|
123
|
+
clearLine(this.stderr);
|
|
124
|
+
this.stderr.write(`${this.format.red("error")} ${msg}\n`);
|
|
1157
125
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
};
|
|
126
|
+
log(msg) {
|
|
127
|
+
clearLine(this.stdout);
|
|
128
|
+
this.stdout.write(`${msg}\n`);
|
|
129
|
+
}
|
|
130
|
+
warn(msg) {
|
|
131
|
+
clearLine(this.stderr);
|
|
132
|
+
this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
|
|
133
|
+
}
|
|
134
|
+
info(msg) {
|
|
135
|
+
clearLine(this.stdout);
|
|
136
|
+
this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
|
|
1169
137
|
}
|
|
1170
|
-
return undefined;
|
|
1171
|
-
}
|
|
1172
|
-
function getGitHubRepoId(repo) {
|
|
1173
|
-
return repo ? `${repo.owner}/${repo.name}` : undefined;
|
|
1174
138
|
}
|
|
1175
139
|
|
|
1176
140
|
class CacheProvider {
|
|
@@ -1230,9 +194,6 @@ class Config {
|
|
|
1230
194
|
cwd = path.resolve(process__default.cwd());
|
|
1231
195
|
configFile = path.join(os.homedir(), ".cals-config.json");
|
|
1232
196
|
cacheDir = cachedir("cals-cli");
|
|
1233
|
-
agent = new https.Agent({
|
|
1234
|
-
keepAlive: true,
|
|
1235
|
-
});
|
|
1236
197
|
configCached = undefined;
|
|
1237
198
|
get config() {
|
|
1238
199
|
const existingConfig = this.configCached;
|
|
@@ -1275,987 +236,316 @@ class Config {
|
|
|
1275
236
|
}
|
|
1276
237
|
}
|
|
1277
238
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
readline.clearLine(stdout, CLEAR_WHOLE_LINE);
|
|
1281
|
-
readline.cursorTo(stdout, 0);
|
|
1282
|
-
}
|
|
1283
|
-
class Reporter {
|
|
1284
|
-
constructor(opts = {}) {
|
|
1285
|
-
this.nonInteractive = !!opts.nonInteractive;
|
|
1286
|
-
this.isVerbose = !!opts.verbose;
|
|
1287
|
-
}
|
|
1288
|
-
stdout = process__default.stdout;
|
|
1289
|
-
stderr = process__default.stderr;
|
|
1290
|
-
nonInteractive;
|
|
1291
|
-
isVerbose;
|
|
1292
|
-
format = chalk;
|
|
1293
|
-
error(msg) {
|
|
1294
|
-
clearLine(this.stderr);
|
|
1295
|
-
this.stderr.write(`${this.format.red("error")} ${msg}\n`);
|
|
1296
|
-
}
|
|
1297
|
-
log(msg) {
|
|
1298
|
-
clearLine(this.stdout);
|
|
1299
|
-
this.stdout.write(`${msg}\n`);
|
|
1300
|
-
}
|
|
1301
|
-
warn(msg) {
|
|
1302
|
-
clearLine(this.stderr);
|
|
1303
|
-
this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
|
|
1304
|
-
}
|
|
1305
|
-
info(msg) {
|
|
1306
|
-
clearLine(this.stdout);
|
|
1307
|
-
this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
function createReporter(argv) {
|
|
1312
|
-
return new Reporter({
|
|
1313
|
-
verbose: !!argv.verbose,
|
|
1314
|
-
nonInteractive: !!argv.nonInteractive,
|
|
1315
|
-
});
|
|
239
|
+
function createReporter() {
|
|
240
|
+
return new Reporter();
|
|
1316
241
|
}
|
|
1317
242
|
function createCacheProvider(config, argv) {
|
|
1318
243
|
const cache = new CacheProvider(config);
|
|
1319
|
-
|
|
1320
|
-
if (argv.validateCache === true) {
|
|
244
|
+
if (argv.noCache === true) {
|
|
1321
245
|
cache.mustValidate = true;
|
|
1322
246
|
}
|
|
1323
|
-
// old option: --no-cache
|
|
1324
|
-
if (argv.cache === false) {
|
|
1325
|
-
deprecate(() => {
|
|
1326
|
-
cache.mustValidate = true;
|
|
1327
|
-
}, "The --no-cache option is deprecated. See new --validate-cache option")();
|
|
1328
|
-
}
|
|
1329
247
|
return cache;
|
|
1330
|
-
}
|
|
1331
|
-
function createConfig() {
|
|
1332
|
-
return new Config();
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
throw Error("Missing --definition-file option");
|
|
1343
|
-
}
|
|
1344
|
-
const definitionFile = argv.definitionFile;
|
|
1345
|
-
if (!fs.existsSync(definitionFile)) {
|
|
1346
|
-
throw Error(`The file ${definitionFile} does not exist`);
|
|
1347
|
-
}
|
|
1348
|
-
return new DefinitionFile(definitionFile);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
async function reportRateLimit(reporter, github, block) {
|
|
1352
|
-
reporter.info(`Rate limit: ${(await github.octokit.rateLimit.get()).data.rate.remaining}`);
|
|
1353
|
-
await block();
|
|
1354
|
-
reporter.info(`Rate limit: ${(await github.octokit.rateLimit.get()).data.rate.remaining}`);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
/**
|
|
1358
|
-
* Reorder a list to preserve same order as it had previously.
|
|
1359
|
-
*
|
|
1360
|
-
* Not a very pretty algorithm but it works for us.
|
|
1361
|
-
*/
|
|
1362
|
-
function reorderListToSimilarAsBefore(oldList, updatedList, selector, insertLast = false) {
|
|
1363
|
-
let result = [];
|
|
1364
|
-
// Keep items present in old list.
|
|
1365
|
-
let remaining1 = [...updatedList];
|
|
1366
|
-
for (const old of oldList) {
|
|
1367
|
-
let found = false;
|
|
1368
|
-
for (const it of remaining1) {
|
|
1369
|
-
if (selector(it) === selector(old)) {
|
|
1370
|
-
found = true;
|
|
1371
|
-
result.push(it);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
if (found) {
|
|
1375
|
-
remaining1 = remaining1.filter((it) => selector(old) !== selector(it));
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
const remaining = updatedList.filter((updated) => !result.some((it) => selector(it) == selector(updated)));
|
|
1379
|
-
if (insertLast) {
|
|
1380
|
-
result.push(...remaining);
|
|
1381
|
-
}
|
|
1382
|
-
else {
|
|
1383
|
-
// Insert remaining at first position by ordering.
|
|
1384
|
-
for (const it of remaining) {
|
|
1385
|
-
let found = false;
|
|
1386
|
-
for (let i = 0; i < result.length; i++) {
|
|
1387
|
-
if (selector(result[i]).localeCompare(selector(it)) > 0) {
|
|
1388
|
-
found = true;
|
|
1389
|
-
result = [...result.slice(0, i), it, ...result.slice(i)];
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (!found) {
|
|
1394
|
-
result.push(it);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
return result;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
async function getReposFromGitHub(github, orgs) {
|
|
1402
|
-
return (await pMap(orgs, async (org) => {
|
|
1403
|
-
const repos = await github.getOrgRepoList({ org: org.login });
|
|
1404
|
-
return pMap(repos, async (repo) => {
|
|
1405
|
-
const detailedRepo = await github.getRepository(repo.owner.login, repo.name);
|
|
1406
|
-
if (detailedRepo === undefined) {
|
|
1407
|
-
throw Error(`Repo not found: ${repo.owner.login}/${repo.name}`);
|
|
1408
|
-
}
|
|
1409
|
-
return {
|
|
1410
|
-
basic: repo,
|
|
1411
|
-
repository: detailedRepo,
|
|
1412
|
-
teams: await github.getRepositoryTeamsList(detailedRepo),
|
|
1413
|
-
};
|
|
1414
|
-
});
|
|
1415
|
-
})).flat();
|
|
1416
|
-
}
|
|
1417
|
-
async function getTeams(github, orgs) {
|
|
1418
|
-
const intermediate = await pMap(orgs, async (org) => {
|
|
1419
|
-
const teams = await github.getTeamList(org);
|
|
1420
|
-
return {
|
|
1421
|
-
org,
|
|
1422
|
-
teams: await pMap(teams, async (team) => ({
|
|
1423
|
-
team,
|
|
1424
|
-
users: await github.getTeamMemberListIncludingInvited(org, team),
|
|
1425
|
-
})),
|
|
1426
|
-
};
|
|
1427
|
-
});
|
|
1428
|
-
// Transform output.
|
|
1429
|
-
return intermediate.reduce((prev, cur) => {
|
|
1430
|
-
prev[cur.org.login] = cur.teams;
|
|
1431
|
-
return prev;
|
|
1432
|
-
}, {});
|
|
1433
|
-
}
|
|
1434
|
-
function getCommonTeams(ownerRepos) {
|
|
1435
|
-
return ownerRepos.length === 0
|
|
1436
|
-
? []
|
|
1437
|
-
: ownerRepos[0].teams.filter((team) => ownerRepos.every((repo) => repo.teams.some((otherTeam) => otherTeam.name === team.name &&
|
|
1438
|
-
otherTeam.permission === team.permission)));
|
|
1439
|
-
}
|
|
1440
|
-
function getSpecificTeams(teams, commonTeams) {
|
|
1441
|
-
return teams.filter((team) => !commonTeams.some((it) => it.name === team.name && it.permission === team.permission));
|
|
1442
|
-
}
|
|
1443
|
-
function getFormattedTeams(oldTeams, teams) {
|
|
1444
|
-
const result = teams.length === 0
|
|
1445
|
-
? undefined
|
|
1446
|
-
: teams.map((it) => ({
|
|
1447
|
-
name: it.name,
|
|
1448
|
-
permission: it.permission,
|
|
1449
|
-
}));
|
|
1450
|
-
return result
|
|
1451
|
-
? reorderListToSimilarAsBefore(oldTeams ?? [], result, (it) => it.name)
|
|
1452
|
-
: undefined;
|
|
1453
|
-
}
|
|
1454
|
-
async function getOrgs(github, orgs) {
|
|
1455
|
-
return pMap(orgs, (it) => github.getOrg(it));
|
|
1456
|
-
}
|
|
1457
|
-
function removeDuplicates(items, selector) {
|
|
1458
|
-
const ids = [];
|
|
1459
|
-
const result = [];
|
|
1460
|
-
for (const item of items) {
|
|
1461
|
-
const id = selector(item);
|
|
1462
|
-
if (!ids.includes(id)) {
|
|
1463
|
-
result.push(item);
|
|
1464
|
-
ids.push(id);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return result;
|
|
1468
|
-
}
|
|
1469
|
-
async function getMembers(github, orgs) {
|
|
1470
|
-
return removeDuplicates((await pMap(orgs, (org) => github.getOrgMembersListIncludingInvited(org.login)))
|
|
1471
|
-
.flat()
|
|
1472
|
-
.map((it) => it.login), (it) => it);
|
|
1473
|
-
}
|
|
1474
|
-
async function getSnykRepos(snyk, definition) {
|
|
1475
|
-
return (await snyk.getProjects(definition))
|
|
1476
|
-
.map((it) => getGitHubRepo(it))
|
|
1477
|
-
.filter((it) => it !== undefined)
|
|
1478
|
-
.map((it) => getRepoId(it.owner, it.name));
|
|
1479
|
-
}
|
|
1480
|
-
async function getProjects(github, orgs, definition, snyk) {
|
|
1481
|
-
const snykReposPromise = getSnykRepos(snyk, definition);
|
|
1482
|
-
const repos = await getReposFromGitHub(github, orgs);
|
|
1483
|
-
const snykRepos = await snykReposPromise;
|
|
1484
|
-
const definitionRepos = Object.fromEntries(getRepos(definition).map((repo) => [repo.id, repo]));
|
|
1485
|
-
const projectGroups = Object.values(repos.reduce((acc, cur) => {
|
|
1486
|
-
const org = cur.repository.owner.login;
|
|
1487
|
-
const repoId = getRepoId(org, cur.repository.name);
|
|
1488
|
-
const projectName = definitionRepos[repoId]?.project?.name ?? "Unknown";
|
|
1489
|
-
const project = acc[projectName] || {
|
|
1490
|
-
name: projectName,
|
|
1491
|
-
definition: definitionRepos[repoId]?.project,
|
|
1492
|
-
repos: [],
|
|
1493
|
-
};
|
|
1494
|
-
return {
|
|
1495
|
-
...acc,
|
|
1496
|
-
[projectName]: {
|
|
1497
|
-
...project,
|
|
1498
|
-
repos: {
|
|
1499
|
-
...project.repos,
|
|
1500
|
-
[org]: [...(project.repos[org] || []), cur],
|
|
1501
|
-
},
|
|
1502
|
-
},
|
|
1503
|
-
};
|
|
1504
|
-
}, {}));
|
|
1505
|
-
const projects = projectGroups.map((project) => {
|
|
1506
|
-
const github = Object.entries(project.repos).map(([org, list]) => {
|
|
1507
|
-
const commonTeams = getCommonTeams(list);
|
|
1508
|
-
const oldOrg = project.definition?.github?.find((it) => it.organization == org);
|
|
1509
|
-
const repos = list.map((repo) => {
|
|
1510
|
-
const repoId = getRepoId(repo.basic.owner.login, repo.basic.name);
|
|
1511
|
-
const definitionRepo = definitionRepos[repoId];
|
|
1512
|
-
const result = {
|
|
1513
|
-
name: repo.basic.name,
|
|
1514
|
-
previousNames: definitionRepo?.repo.previousNames,
|
|
1515
|
-
archived: repo.repository.archived ? true : undefined,
|
|
1516
|
-
issues: repo.repository.has_issues ? undefined : false,
|
|
1517
|
-
wiki: repo.repository.has_wiki ? undefined : false,
|
|
1518
|
-
teams: getFormattedTeams(definitionRepo?.repo?.teams ?? [], getSpecificTeams(repo.teams, commonTeams)),
|
|
1519
|
-
snyk: snykRepos.includes(repoId) ? true : undefined,
|
|
1520
|
-
public: repo.repository.private ? undefined : true,
|
|
1521
|
-
responsible: definitionRepo?.repo.responsible,
|
|
1522
|
-
};
|
|
1523
|
-
// Try to preserve property order.
|
|
1524
|
-
return Object.fromEntries(reorderListToSimilarAsBefore(definitionRepo ? Object.entries(definitionRepo.repo) : [], Object.entries(result), (it) => it[0], true));
|
|
1525
|
-
});
|
|
1526
|
-
const teams = getFormattedTeams(oldOrg?.teams ?? [], commonTeams);
|
|
1527
|
-
return {
|
|
1528
|
-
organization: org,
|
|
1529
|
-
teams: teams,
|
|
1530
|
-
repos: reorderListToSimilarAsBefore(oldOrg?.repos ?? [], repos, (it) => it.name),
|
|
1531
|
-
};
|
|
1532
|
-
});
|
|
1533
|
-
return {
|
|
1534
|
-
name: project.name,
|
|
1535
|
-
github: reorderListToSimilarAsBefore(project.definition?.github ?? [], github, (it) => it.organization),
|
|
1536
|
-
};
|
|
1537
|
-
});
|
|
1538
|
-
return reorderListToSimilarAsBefore(definition.projects, projects, (it) => it.name);
|
|
1539
|
-
}
|
|
1540
|
-
function buildGitHubTeamsList(definition, list) {
|
|
1541
|
-
const result = Object.entries(list).map(([org, teams]) => ({
|
|
1542
|
-
organization: org,
|
|
1543
|
-
teams: teams.map((team) => ({
|
|
1544
|
-
name: team.team.name,
|
|
1545
|
-
members: team.users
|
|
1546
|
-
.map((it) => it.login)
|
|
1547
|
-
.sort((a, b) => a.localeCompare(b)),
|
|
1548
|
-
})),
|
|
1549
|
-
}));
|
|
1550
|
-
return reorderListToSimilarAsBefore(definition.github.teams, result, (it) => it.organization);
|
|
1551
|
-
}
|
|
1552
|
-
function buildGitHubUsersList(definition, members) {
|
|
1553
|
-
const result = members.map((memberLogin) => definition.github.users.find((user) => user.login === memberLogin) || {
|
|
1554
|
-
type: "external",
|
|
1555
|
-
login: memberLogin,
|
|
1556
|
-
// TODO: Fetch name from GitHub?
|
|
1557
|
-
name: "*Unknown*",
|
|
1558
|
-
});
|
|
1559
|
-
return reorderListToSimilarAsBefore(definition.github.users, result, (it) => it.login);
|
|
1560
|
-
}
|
|
1561
|
-
async function dumpSetup(_config, reporter, github, snyk, outfile, definitionFile) {
|
|
1562
|
-
reporter.info("Fetching data. This might take some time");
|
|
1563
|
-
const definition = await definitionFile.getDefinition();
|
|
1564
|
-
const orgs = await getOrgs(github, getGitHubOrgs(definition));
|
|
1565
|
-
const teams = getTeams(github, orgs);
|
|
1566
|
-
const members = getMembers(github, orgs);
|
|
1567
|
-
const projects = getProjects(github, orgs, definition, snyk);
|
|
1568
|
-
const generatedDefinition = {
|
|
1569
|
-
snyk: definition.snyk,
|
|
1570
|
-
github: {
|
|
1571
|
-
users: buildGitHubUsersList(definition, await members),
|
|
1572
|
-
teams: buildGitHubTeamsList(definition, await teams),
|
|
1573
|
-
},
|
|
1574
|
-
projects: await projects,
|
|
1575
|
-
};
|
|
1576
|
-
// TODO: An earlier version we had preserved comments by using yawn-yaml
|
|
1577
|
-
// package. However it often produced invalid yaml, so we have removed
|
|
1578
|
-
// it. We might want to revisit it to preserve comments.
|
|
1579
|
-
const doc = yaml.load(await definitionFile.getContents());
|
|
1580
|
-
doc.snyk = generatedDefinition.snyk;
|
|
1581
|
-
doc.projects = generatedDefinition.projects;
|
|
1582
|
-
doc.github = generatedDefinition.github;
|
|
1583
|
-
// Convert to/from plain JSON so that undefined elements are removed.
|
|
1584
|
-
fs.writeFileSync(outfile, yaml.dump(JSON.parse(JSON.stringify(doc))));
|
|
1585
|
-
reporter.info(`Saved to ${outfile}`);
|
|
1586
|
-
reporter.info(`Number of GitHub requests: ${github.requestCount}`);
|
|
1587
|
-
}
|
|
1588
|
-
const command$h = {
|
|
1589
|
-
command: "dump-setup",
|
|
1590
|
-
describe: "Dump active setup as YAML. Will be formated same as the definition file.",
|
|
1591
|
-
builder: (yargs) => yargs
|
|
1592
|
-
.positional("outfile", {
|
|
1593
|
-
type: "string",
|
|
1594
|
-
})
|
|
1595
|
-
.option(definitionFileOptionName, definitionFileOptionValue)
|
|
1596
|
-
.demandOption("outfile"),
|
|
1597
|
-
handler: async (argv) => {
|
|
1598
|
-
const reporter = createReporter(argv);
|
|
1599
|
-
const config = createConfig();
|
|
1600
|
-
const github = await createGitHubService({
|
|
1601
|
-
config,
|
|
1602
|
-
cache: createCacheProvider(config, argv),
|
|
1603
|
-
});
|
|
1604
|
-
const snyk = createSnykService({ config });
|
|
1605
|
-
await reportRateLimit(reporter, github, () => dumpSetup(config, reporter, github, snyk, argv.outfile, getDefinitionFile(argv)));
|
|
1606
|
-
},
|
|
1607
|
-
};
|
|
1608
|
-
|
|
1609
|
-
const command$g = {
|
|
1610
|
-
command: "validate",
|
|
1611
|
-
describe: "Validate definition file.",
|
|
1612
|
-
builder: (yargs) => yargs.option(definitionFileOptionName, definitionFileOptionValue),
|
|
1613
|
-
handler: async (argv) => {
|
|
1614
|
-
const reporter = createReporter(argv);
|
|
1615
|
-
await getDefinitionFile(argv).getDefinition();
|
|
1616
|
-
reporter.info("Valid!");
|
|
1617
|
-
},
|
|
1618
|
-
};
|
|
1619
|
-
|
|
1620
|
-
const command$f = {
|
|
1621
|
-
command: "definition",
|
|
1622
|
-
describe: "CALS definition file management",
|
|
1623
|
-
builder: (yargs) => yargs
|
|
1624
|
-
.command(command$h)
|
|
1625
|
-
.command(command$g)
|
|
1626
|
-
.demandCommand()
|
|
1627
|
-
.usage("cals definition"),
|
|
1628
|
-
handler: () => {
|
|
1629
|
-
yargs(hideBin(process__default.argv)).showHelp();
|
|
1630
|
-
},
|
|
1631
|
-
};
|
|
1632
|
-
|
|
1633
|
-
const command$e = {
|
|
1634
|
-
command: "delete-cache",
|
|
1635
|
-
describe: "Delete cached data",
|
|
1636
|
-
handler: (argv) => {
|
|
1637
|
-
const config = createConfig();
|
|
1638
|
-
const cache = createCacheProvider(config, argv);
|
|
1639
|
-
const reporter = createReporter(argv);
|
|
1640
|
-
cache.cleanup();
|
|
1641
|
-
reporter.info("Cache deleted");
|
|
1642
|
-
},
|
|
1643
|
-
};
|
|
1644
|
-
|
|
1645
|
-
const command$d = {
|
|
1646
|
-
command: "getting-started",
|
|
1647
|
-
describe: "Getting started",
|
|
1648
|
-
handler: (argv) => {
|
|
1649
|
-
const reporter = createReporter(argv);
|
|
1650
|
-
reporter.log("For getting started, see https://liflig.atlassian.net/wiki/x/E8MNAQ");
|
|
1651
|
-
},
|
|
1652
|
-
};
|
|
1653
|
-
|
|
1654
|
-
async function analyzeDirectory(reporter, config, github, org) {
|
|
1655
|
-
const repos = await github.getOrgRepoList({ org });
|
|
1656
|
-
const reposDict = repos.reduce((acc, cur) => ({ ...acc, [cur.name]: cur }), {});
|
|
1657
|
-
const dirs = fs
|
|
1658
|
-
.readdirSync(config.cwd)
|
|
1659
|
-
.filter((it) => fs.statSync(path.join(config.cwd, it)).isDirectory())
|
|
1660
|
-
// Skip hidden folders
|
|
1661
|
-
.filter((it) => !it.startsWith("."))
|
|
1662
|
-
.sort((a, b) => a.localeCompare(b));
|
|
1663
|
-
const stats = {
|
|
1664
|
-
unknown: 0,
|
|
1665
|
-
archived: 0,
|
|
1666
|
-
ok: 0,
|
|
1667
|
-
};
|
|
1668
|
-
dirs.forEach((it) => {
|
|
1669
|
-
if (!(it in reposDict)) {
|
|
1670
|
-
reporter.warn(sprintf("%-30s <-- Not found in repository list (maybe changed name?)", it));
|
|
1671
|
-
stats.unknown++;
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
if (reposDict[it].isArchived) {
|
|
1675
|
-
reporter.info(sprintf("%-30s <-- Archived", it));
|
|
1676
|
-
stats.archived++;
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
stats.ok += 1;
|
|
1680
|
-
});
|
|
1681
|
-
reporter.info(sprintf("Stats: unknown=%d archived=%d ok=%d", stats.unknown, stats.archived, stats.ok));
|
|
1682
|
-
reporter.info("Use `cals github generate-clone-commands` to check for repositories not checked out");
|
|
1683
|
-
}
|
|
1684
|
-
const command$c = {
|
|
1685
|
-
command: "analyze-directory",
|
|
1686
|
-
describe: "Analyze directory for git repos",
|
|
1687
|
-
builder: (yargs) => yargs.options("org", {
|
|
1688
|
-
required: true,
|
|
1689
|
-
describe: "Specify GitHub organization",
|
|
1690
|
-
type: "string",
|
|
1691
|
-
}),
|
|
1692
|
-
handler: async (argv) => {
|
|
1693
|
-
const config = createConfig();
|
|
1694
|
-
const github = await createGitHubService({
|
|
1695
|
-
config,
|
|
1696
|
-
cache: createCacheProvider(config, argv),
|
|
1697
|
-
});
|
|
1698
|
-
const reporter = createReporter(argv);
|
|
1699
|
-
return analyzeDirectory(reporter, config, github, argv.org);
|
|
1700
|
-
},
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
function getChangedRepoAttribs(definitionRepo, actualRepo) {
|
|
1704
|
-
const attribs = [];
|
|
1705
|
-
const archived = definitionRepo.archived || false;
|
|
1706
|
-
if (archived !== actualRepo.archived) {
|
|
1707
|
-
attribs.push({
|
|
1708
|
-
archived,
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
const issues = definitionRepo.issues ?? true;
|
|
1712
|
-
if (issues !== actualRepo.has_issues && !actualRepo.archived) {
|
|
1713
|
-
attribs.push({
|
|
1714
|
-
issues,
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
const wiki = definitionRepo.wiki ?? true;
|
|
1718
|
-
if (wiki !== actualRepo.has_wiki && !actualRepo.archived) {
|
|
1719
|
-
attribs.push({
|
|
1720
|
-
wiki,
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
const isPrivate = definitionRepo.public !== true;
|
|
1724
|
-
if (isPrivate !== actualRepo.private) {
|
|
1725
|
-
attribs.push({
|
|
1726
|
-
private: isPrivate,
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
return attribs;
|
|
1730
|
-
}
|
|
1731
|
-
/**
|
|
1732
|
-
* Get teams from both the project level and the repository level
|
|
1733
|
-
* and ensure that repository level override project level.
|
|
1734
|
-
*/
|
|
1735
|
-
function getExpectedTeams(projectTeams, repoTeams) {
|
|
1736
|
-
return [
|
|
1737
|
-
...repoTeams,
|
|
1738
|
-
...projectTeams.filter((it) => !repoTeams.find((repoTeam) => repoTeam.name === it.name)),
|
|
1739
|
-
];
|
|
1740
|
-
}
|
|
1741
|
-
async function getRepoTeamChanges({ github, org, projectRepo, repo, }) {
|
|
1742
|
-
const changes = [];
|
|
1743
|
-
const expectedTeams = getExpectedTeams(org.teams ?? [], projectRepo.teams ?? []);
|
|
1744
|
-
const existingTeams = await github.getRepositoryTeamsList(repo);
|
|
1745
|
-
// Check for teams to be added / modified.
|
|
1746
|
-
for (const repoteam of expectedTeams) {
|
|
1747
|
-
const found = existingTeams.find((it) => repoteam.name === it.name);
|
|
1748
|
-
if (found !== undefined) {
|
|
1749
|
-
if (found.permission !== repoteam.permission) {
|
|
1750
|
-
changes.push({
|
|
1751
|
-
type: "repo-team-permission",
|
|
1752
|
-
org: org.organization,
|
|
1753
|
-
repo: repo.name,
|
|
1754
|
-
team: found.name,
|
|
1755
|
-
permission: repoteam.permission,
|
|
1756
|
-
current: {
|
|
1757
|
-
permission: found.permission,
|
|
1758
|
-
},
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
changes.push({
|
|
1764
|
-
type: "repo-team-add",
|
|
1765
|
-
org: org.organization,
|
|
1766
|
-
repo: repo.name,
|
|
1767
|
-
team: repoteam.name,
|
|
1768
|
-
permission: repoteam.permission,
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
// Check for teams that should not be registered.
|
|
1773
|
-
for (const team of existingTeams) {
|
|
1774
|
-
if (!expectedTeams.some((it) => team.name === it.name)) {
|
|
1775
|
-
changes.push({
|
|
1776
|
-
type: "repo-team-remove",
|
|
1777
|
-
org: org.organization,
|
|
1778
|
-
repo: repo.name,
|
|
1779
|
-
team: team.name,
|
|
1780
|
-
});
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
return changes;
|
|
1784
|
-
}
|
|
1785
|
-
async function getProjectRepoChanges({ github, org, projectRepo, }) {
|
|
1786
|
-
const changes = [];
|
|
1787
|
-
const repo = await github.getRepository(org.organization, projectRepo.name);
|
|
1788
|
-
if (repo === undefined) {
|
|
1789
|
-
changes.push({
|
|
1790
|
-
type: "repo-create",
|
|
1791
|
-
org: org.organization,
|
|
1792
|
-
repo: projectRepo.name,
|
|
1793
|
-
});
|
|
1794
|
-
return changes;
|
|
1795
|
-
}
|
|
1796
|
-
const attribs = getChangedRepoAttribs(projectRepo, repo);
|
|
1797
|
-
if (attribs.length > 0) {
|
|
1798
|
-
changes.push({
|
|
1799
|
-
type: "repo-update",
|
|
1800
|
-
org: org.organization,
|
|
1801
|
-
repo: repo.name,
|
|
1802
|
-
attribs,
|
|
1803
|
-
});
|
|
1804
|
-
}
|
|
1805
|
-
changes.push(...(await getRepoTeamChanges({
|
|
1806
|
-
github,
|
|
1807
|
-
org,
|
|
1808
|
-
projectRepo,
|
|
1809
|
-
repo,
|
|
1810
|
-
})));
|
|
1811
|
-
return changes;
|
|
1812
|
-
}
|
|
1813
|
-
/**
|
|
1814
|
-
* Generate change set items for projects.
|
|
1815
|
-
*/
|
|
1816
|
-
async function createChangeSetItemsForProjects(github, definition, limitToOrg) {
|
|
1817
|
-
const changes = [];
|
|
1818
|
-
const orgs = definition.projects
|
|
1819
|
-
.flatMap((it) => it.github)
|
|
1820
|
-
.filter((org) => limitToOrg === undefined || limitToOrg === org.organization);
|
|
1821
|
-
changes.push(...(await pMap(orgs, async (org) => pMap(org.repos || [], (projectRepo) => getProjectRepoChanges({
|
|
1822
|
-
github,
|
|
1823
|
-
org,
|
|
1824
|
-
projectRepo,
|
|
1825
|
-
}))))
|
|
1826
|
-
.flat()
|
|
1827
|
-
.flat());
|
|
1828
|
-
return changes;
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* Get user list based on team memberships in an organization.
|
|
1832
|
-
*/
|
|
1833
|
-
function getUsersForOrg(definition, org) {
|
|
1834
|
-
const teams = definition.github.teams.find((it) => it.organization == org);
|
|
1835
|
-
if (teams === undefined)
|
|
1836
|
-
return [];
|
|
1837
|
-
const memberLogins = new Set(teams.teams.flatMap((it) => it.members));
|
|
1838
|
-
return definition.github.users.filter((user) => memberLogins.has(user.login));
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Generate change set items for organization members.
|
|
1842
|
-
*/
|
|
1843
|
-
async function createChangeSetItemsForMembers(github, definition, org) {
|
|
1844
|
-
const changes = [];
|
|
1845
|
-
const users = getUsersForOrg(definition, org.login);
|
|
1846
|
-
const usersLogins = users.map((it) => it.login);
|
|
1847
|
-
const foundLogins = [];
|
|
1848
|
-
const members = await github.getOrgMembersListIncludingInvited(org.login);
|
|
1849
|
-
members.forEach((user) => {
|
|
1850
|
-
if (usersLogins.includes(user.login)) {
|
|
1851
|
-
foundLogins.push(user.login);
|
|
1852
|
-
}
|
|
1853
|
-
else {
|
|
1854
|
-
changes.push({
|
|
1855
|
-
type: "member-remove",
|
|
1856
|
-
org: org.login,
|
|
1857
|
-
user: user.login,
|
|
1858
|
-
});
|
|
1859
|
-
}
|
|
1860
|
-
});
|
|
1861
|
-
for (const user of users.filter((it) => !foundLogins.includes(it.login))) {
|
|
1862
|
-
changes.push({
|
|
1863
|
-
type: "member-add",
|
|
1864
|
-
org: org.login,
|
|
1865
|
-
user: user.login,
|
|
248
|
+
}
|
|
249
|
+
function createConfig() {
|
|
250
|
+
return new Config();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function authenticate({ reporter, token, tokenProvider, }) {
|
|
254
|
+
if (token === undefined) {
|
|
255
|
+
reporter.info("Need API token to talk to GitHub");
|
|
256
|
+
reporter.info("https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook");
|
|
257
|
+
const inputToken = await readInput({
|
|
258
|
+
prompt: "Enter GitHub token: ",
|
|
259
|
+
silent: true,
|
|
1866
260
|
});
|
|
261
|
+
token = inputToken;
|
|
1867
262
|
}
|
|
1868
|
-
|
|
263
|
+
await tokenProvider.setToken(token);
|
|
264
|
+
reporter.info("Token saved to keychain");
|
|
1869
265
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
.filter((it) => !wantedTeamNames.includes(it.name))
|
|
1883
|
-
.forEach((it) => {
|
|
1884
|
-
changes.push({
|
|
1885
|
-
type: "team-remove",
|
|
1886
|
-
org: org.login,
|
|
1887
|
-
team: it.name,
|
|
1888
|
-
});
|
|
1889
|
-
});
|
|
1890
|
-
teams
|
|
1891
|
-
.filter((it) => !actualTeamNames.includes(it.name))
|
|
1892
|
-
.forEach((team) => {
|
|
1893
|
-
changes.push({
|
|
1894
|
-
type: "team-add",
|
|
1895
|
-
org: org.login,
|
|
1896
|
-
team: team.name,
|
|
266
|
+
const command$5 = {
|
|
267
|
+
command: "auth [token]",
|
|
268
|
+
describe: "Authenticate with GitHub",
|
|
269
|
+
builder: (yargs) => yargs.positional("token", {
|
|
270
|
+
describe: "GitHub token (prompted if not provided)",
|
|
271
|
+
type: "string",
|
|
272
|
+
}),
|
|
273
|
+
handler: async (argv) => {
|
|
274
|
+
await authenticate({
|
|
275
|
+
reporter: createReporter(),
|
|
276
|
+
token: argv.token,
|
|
277
|
+
tokenProvider: new GitHubTokenCliProvider(),
|
|
1897
278
|
});
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
class GitHubService {
|
|
283
|
+
octokit;
|
|
284
|
+
cache;
|
|
285
|
+
tokenProvider;
|
|
286
|
+
semaphore;
|
|
287
|
+
constructor(props) {
|
|
288
|
+
this.octokit = props.octokit;
|
|
289
|
+
this.cache = props.cache;
|
|
290
|
+
this.tokenProvider = props.tokenProvider;
|
|
291
|
+
// Control concurrency to GitHub API at service level so we
|
|
292
|
+
// can maximize concurrency all other places.
|
|
293
|
+
this.semaphore = pLimit(6);
|
|
294
|
+
this.octokit.hook.wrap("request", async (request, options) => {
|
|
295
|
+
if (options.method !== "GET") {
|
|
296
|
+
return this.semaphore(() => request(options));
|
|
297
|
+
}
|
|
298
|
+
// Try to cache ETag for GET requests to save on rate limiting.
|
|
299
|
+
// Hits on ETag does not count towards rate limiting.
|
|
300
|
+
const rest = {
|
|
301
|
+
...options,
|
|
302
|
+
};
|
|
303
|
+
delete rest.method;
|
|
304
|
+
delete rest.baseUrl;
|
|
305
|
+
delete rest.headers;
|
|
306
|
+
delete rest.mediaType;
|
|
307
|
+
delete rest.request;
|
|
308
|
+
// Build a key that is used to identify this request.
|
|
309
|
+
const key = Buffer.from(JSON.stringify(rest)).toString("base64");
|
|
310
|
+
const cacheItem = this.cache.retrieveJson(key);
|
|
311
|
+
if (cacheItem !== undefined) {
|
|
312
|
+
// Copying doesn't work, seems we need to mutate this.
|
|
313
|
+
options.headers["If-None-Match"] = cacheItem.data.etag;
|
|
314
|
+
}
|
|
315
|
+
const getResponse = async (allowRetry = true) => {
|
|
316
|
+
try {
|
|
317
|
+
return await request(options);
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
// Handle no change in ETag.
|
|
321
|
+
if (e.status === 304) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
// GitHub seems to throw a lot of 502 errors.
|
|
325
|
+
// Let's give it a few seconds and retry one time.
|
|
326
|
+
if (e.status === 502 && allowRetry) {
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
328
|
+
return await getResponse(false);
|
|
329
|
+
}
|
|
330
|
+
throw e;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const response = await this.semaphore(async () => {
|
|
334
|
+
return getResponse();
|
|
1922
335
|
});
|
|
336
|
+
if (response === undefined) {
|
|
337
|
+
// Undefined is returned for cached data.
|
|
338
|
+
if (cacheItem === undefined) {
|
|
339
|
+
throw new Error("Missing expected cache item");
|
|
340
|
+
}
|
|
341
|
+
// Use previous value.
|
|
342
|
+
return cacheItem.data.data;
|
|
343
|
+
}
|
|
344
|
+
// New value. Store Etag.
|
|
345
|
+
if (response.headers.etag) {
|
|
346
|
+
this.cache.storeJson(key, {
|
|
347
|
+
etag: response.headers.etag,
|
|
348
|
+
data: response,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return response;
|
|
1923
352
|
});
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
353
|
+
}
|
|
354
|
+
async runGraphqlQuery(query) {
|
|
355
|
+
const token = await this.tokenProvider.getToken();
|
|
356
|
+
if (token === undefined) {
|
|
357
|
+
throw new Error("Missing token for GitHub");
|
|
358
|
+
}
|
|
359
|
+
const url = "https://api.github.com/graphql";
|
|
360
|
+
const headers = {
|
|
361
|
+
Authorization: `Bearer ${token}`,
|
|
362
|
+
};
|
|
363
|
+
let requestDuration = -1;
|
|
364
|
+
const response = await this.semaphore(() => {
|
|
365
|
+
const requestStart = performance.now();
|
|
366
|
+
const result = fetch(url, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers,
|
|
369
|
+
body: JSON.stringify({ query }),
|
|
1935
370
|
});
|
|
371
|
+
requestDuration = performance.now() - requestStart;
|
|
372
|
+
return result;
|
|
1936
373
|
});
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
}
|
|
1941
|
-
/**
|
|
1942
|
-
* Remove redundant change set items due to effects by other
|
|
1943
|
-
* change set items.
|
|
1944
|
-
*/
|
|
1945
|
-
function cleanupChangeSetItems(items) {
|
|
1946
|
-
const hasTeamRemove = ({ org, team }) => items.some((it) => it.type === "team-remove" && it.org === org && it.team === team);
|
|
1947
|
-
const hasMemberRemove = ({ org }) => items.some((it) => it.type === "member-remove" && it.org === org);
|
|
1948
|
-
return items.filter((item) => !((item.type === "team-member-remove" && hasTeamRemove(item)) ||
|
|
1949
|
-
(item.type === "repo-team-remove" && hasTeamRemove(item)) ||
|
|
1950
|
-
(item.type === "team-member-remove" && hasMemberRemove(item))));
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
function buildLookup(github) {
|
|
1954
|
-
// We operate using the Octokit SDK, so cache the objects to avoid
|
|
1955
|
-
// excessive lookups to the API for them.
|
|
1956
|
-
const orgCache = {};
|
|
1957
|
-
const orgTeamListCache = {};
|
|
1958
|
-
async function getOrg(orgName) {
|
|
1959
|
-
if (!(orgName in orgCache)) {
|
|
1960
|
-
orgCache[orgName] = await github.getOrg(orgName);
|
|
374
|
+
if (response.status === 401) {
|
|
375
|
+
process.stderr.write("Unauthorized\n");
|
|
376
|
+
await this.tokenProvider.markInvalid();
|
|
1961
377
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
if (!(orgName in orgTeamListCache)) {
|
|
1966
|
-
const org = await getOrg(orgName);
|
|
1967
|
-
orgTeamListCache[orgName] = await github.getTeamList(org);
|
|
378
|
+
// If you get 502 after 10s, it is a timeout.
|
|
379
|
+
if (response.status === 502) {
|
|
380
|
+
throw new Error(`Response from Github likely timed out (10s max) after elapsed ${requestDuration}ms with status ${response.status}: ${await response.text()}`);
|
|
1968
381
|
}
|
|
1969
|
-
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new Error(`Response from GitHub not OK (${response.status}): ${await response.text()}`);
|
|
384
|
+
}
|
|
385
|
+
const json = (await response.json());
|
|
386
|
+
if (json.errors) {
|
|
387
|
+
throw new Error(`Error from GitHub GraphQL API: ${JSON.stringify(json.errors)}`);
|
|
388
|
+
}
|
|
389
|
+
if (json.data == null) {
|
|
390
|
+
throw new Error("No data received from GitHub GraphQL API (unknown reason)");
|
|
391
|
+
}
|
|
392
|
+
return json.data;
|
|
1970
393
|
}
|
|
1971
|
-
async
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
394
|
+
async getOrgRepoList({ org }) {
|
|
395
|
+
const getQuery = (after) => `{
|
|
396
|
+
organization(login: "${org}") {
|
|
397
|
+
repositories(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
398
|
+
totalCount
|
|
399
|
+
pageInfo {
|
|
400
|
+
hasNextPage
|
|
401
|
+
endCursor
|
|
402
|
+
}
|
|
403
|
+
nodes {
|
|
404
|
+
name
|
|
405
|
+
owner {
|
|
406
|
+
login
|
|
407
|
+
}
|
|
408
|
+
defaultBranchRef {
|
|
409
|
+
name
|
|
410
|
+
}
|
|
411
|
+
createdAt
|
|
412
|
+
updatedAt
|
|
413
|
+
isArchived
|
|
414
|
+
sshUrl
|
|
415
|
+
repositoryTopics(first: 100) {
|
|
416
|
+
edges {
|
|
417
|
+
node {
|
|
418
|
+
topic {
|
|
419
|
+
name
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
1976
423
|
}
|
|
1977
|
-
|
|
424
|
+
}
|
|
1978
425
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
/**
|
|
1990
|
-
* Execute a change set item.
|
|
1991
|
-
*/
|
|
1992
|
-
async function executeChangeSetItem(github, changeItem, reporter, lookup) {
|
|
1993
|
-
// We return to ensure all code paths are followed during compiling.
|
|
1994
|
-
// If a change item type is missing we will get a compile error.
|
|
1995
|
-
if (isNotImplementedChangeSetItem(changeItem)) {
|
|
1996
|
-
reporter.warn("Not currently implemented - do it manually");
|
|
1997
|
-
return true;
|
|
1998
|
-
}
|
|
1999
|
-
switch (changeItem.type) {
|
|
2000
|
-
case "member-remove":
|
|
2001
|
-
await github.octokit.orgs.removeMembershipForUser({
|
|
2002
|
-
org: changeItem.org,
|
|
2003
|
-
username: changeItem.user,
|
|
2004
|
-
});
|
|
2005
|
-
return true;
|
|
2006
|
-
case "member-add":
|
|
2007
|
-
await github.octokit.orgs.setMembershipForUser({
|
|
2008
|
-
org: changeItem.org,
|
|
2009
|
-
username: changeItem.user,
|
|
2010
|
-
role: "member",
|
|
2011
|
-
});
|
|
2012
|
-
return true;
|
|
2013
|
-
case "team-remove":
|
|
2014
|
-
await github.octokit.teams.deleteInOrg({
|
|
2015
|
-
org: changeItem.org,
|
|
2016
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2017
|
-
.slug,
|
|
2018
|
-
});
|
|
2019
|
-
return true;
|
|
2020
|
-
case "team-add":
|
|
2021
|
-
await github.octokit.teams.create({
|
|
2022
|
-
org: changeItem.org,
|
|
2023
|
-
name: changeItem.team,
|
|
2024
|
-
privacy: "closed",
|
|
2025
|
-
});
|
|
2026
|
-
return true;
|
|
2027
|
-
case "team-member-permission":
|
|
2028
|
-
await github.octokit.teams.addOrUpdateMembershipForUserInOrg({
|
|
2029
|
-
org: changeItem.org,
|
|
2030
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2031
|
-
.slug,
|
|
2032
|
-
username: changeItem.user,
|
|
2033
|
-
role: changeItem.role,
|
|
2034
|
-
});
|
|
2035
|
-
return true;
|
|
2036
|
-
case "team-member-remove":
|
|
2037
|
-
await github.octokit.teams.removeMembershipForUserInOrg({
|
|
2038
|
-
org: changeItem.org,
|
|
2039
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2040
|
-
.slug,
|
|
2041
|
-
username: changeItem.user,
|
|
2042
|
-
});
|
|
2043
|
-
return true;
|
|
2044
|
-
case "team-member-add":
|
|
2045
|
-
await github.octokit.teams.addOrUpdateMembershipForUserInOrg({
|
|
2046
|
-
org: changeItem.org,
|
|
2047
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2048
|
-
.slug,
|
|
2049
|
-
username: changeItem.user,
|
|
2050
|
-
});
|
|
2051
|
-
return true;
|
|
2052
|
-
case "repo-update": {
|
|
2053
|
-
const upd = {
|
|
2054
|
-
owner: changeItem.org,
|
|
2055
|
-
repo: changeItem.repo,
|
|
2056
|
-
};
|
|
2057
|
-
for (const attrib of changeItem.attribs) {
|
|
2058
|
-
if ("archived" in attrib) {
|
|
2059
|
-
upd.archived = attrib.archived;
|
|
2060
|
-
}
|
|
2061
|
-
else if ("issues" in attrib) {
|
|
2062
|
-
upd.has_issues = attrib.issues;
|
|
426
|
+
}
|
|
427
|
+
}`;
|
|
428
|
+
return this.cache.json(`repos-${org}`, async () => {
|
|
429
|
+
const repos = [];
|
|
430
|
+
let after = null;
|
|
431
|
+
while (true) {
|
|
432
|
+
const query = getQuery(after);
|
|
433
|
+
const res = await this.runGraphqlQuery(query);
|
|
434
|
+
if (res.organization == null) {
|
|
435
|
+
throw new Error("Missing organization");
|
|
2063
436
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
437
|
+
if (res.organization.repositories.nodes == null) {
|
|
438
|
+
throw new Error("Missing organization nodes");
|
|
2066
439
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
440
|
+
repos.push(...res.organization.repositories.nodes);
|
|
441
|
+
if (!res.organization.repositories.pageInfo.hasNextPage) {
|
|
442
|
+
break;
|
|
2069
443
|
}
|
|
444
|
+
after = res.organization.repositories.pageInfo.endCursor;
|
|
2070
445
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
}
|
|
2074
|
-
case "repo-team-remove":
|
|
2075
|
-
await github.octokit.teams.removeRepoInOrg({
|
|
2076
|
-
org: changeItem.org,
|
|
2077
|
-
owner: changeItem.org,
|
|
2078
|
-
repo: changeItem.repo,
|
|
2079
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2080
|
-
.slug,
|
|
2081
|
-
});
|
|
2082
|
-
return true;
|
|
2083
|
-
case "repo-team-add":
|
|
2084
|
-
case "repo-team-permission":
|
|
2085
|
-
await github.octokit.teams.addOrUpdateRepoPermissionsInOrg({
|
|
2086
|
-
org: changeItem.org,
|
|
2087
|
-
owner: changeItem.org,
|
|
2088
|
-
repo: changeItem.repo,
|
|
2089
|
-
team_slug: (await lookup.getOrgTeam(changeItem.org, changeItem.team))
|
|
2090
|
-
.slug,
|
|
2091
|
-
permission: changeItem.permission,
|
|
2092
|
-
});
|
|
2093
|
-
return true;
|
|
446
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
447
|
+
});
|
|
2094
448
|
}
|
|
2095
449
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
const lookup = buildLookup(github);
|
|
2101
|
-
for (const changeItem of changes) {
|
|
2102
|
-
reporter.info(`Executing ${JSON.stringify(changeItem)}`);
|
|
2103
|
-
await executeChangeSetItem(github, changeItem, reporter, lookup);
|
|
2104
|
-
}
|
|
450
|
+
async function createOctokit(tokenProvider) {
|
|
451
|
+
return new Octokit({
|
|
452
|
+
auth: await tokenProvider.getToken(),
|
|
453
|
+
});
|
|
2105
454
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
function getSemaphore(orgName) {
|
|
2113
|
-
if (!(orgName in semaphores)) {
|
|
2114
|
-
semaphores[orgName] = pLimit(1);
|
|
2115
|
-
}
|
|
2116
|
-
return semaphores[orgName];
|
|
2117
|
-
}
|
|
2118
|
-
return async (orgName) => await getSemaphore(orgName)(async () => {
|
|
2119
|
-
if (!(orgName in orgs)) {
|
|
2120
|
-
const org = await github.getOrg(orgName);
|
|
2121
|
-
orgs[orgName] = {
|
|
2122
|
-
org,
|
|
2123
|
-
teams: await github.getTeamList(org),
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
return orgs[orgName];
|
|
455
|
+
async function createGitHubService(props) {
|
|
456
|
+
const tokenProvider = props.tokenProvider ?? new GitHubTokenCliProvider();
|
|
457
|
+
return new GitHubService({
|
|
458
|
+
octokit: await createOctokit(tokenProvider),
|
|
459
|
+
cache: props.cache,
|
|
460
|
+
tokenProvider,
|
|
2127
461
|
});
|
|
2128
462
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
463
|
+
|
|
464
|
+
function getGroup(repo) {
|
|
465
|
+
const projectTopics = [];
|
|
466
|
+
let isInfra = false;
|
|
467
|
+
repo.repositoryTopics.edges.forEach((edge) => {
|
|
468
|
+
const name = edge.node.topic.name;
|
|
469
|
+
if (name.startsWith("customer-")) {
|
|
470
|
+
projectTopics.push(name.substring(9));
|
|
2134
471
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
...changes,
|
|
2138
|
-
...(await createChangeSetItemsForMembers(github, definition, org)),
|
|
2139
|
-
];
|
|
2140
|
-
changes = [
|
|
2141
|
-
...changes,
|
|
2142
|
-
...(await createChangeSetItemsForTeams(github, definition, org)),
|
|
2143
|
-
];
|
|
2144
|
-
}
|
|
2145
|
-
changes = [
|
|
2146
|
-
...changes,
|
|
2147
|
-
...(await createChangeSetItemsForProjects(github, definition, limitToOrg)),
|
|
2148
|
-
];
|
|
2149
|
-
changes = cleanupChangeSetItems(changes);
|
|
2150
|
-
const ignored = changes.filter(isNotImplementedChangeSetItem);
|
|
2151
|
-
changes = changes.filter((it) => !ignored.includes(it));
|
|
2152
|
-
if (ignored.length > 0) {
|
|
2153
|
-
reporter.info("Not implemented:");
|
|
2154
|
-
for (const change of ignored) {
|
|
2155
|
-
reporter.info(` - ${JSON.stringify(change)}`);
|
|
472
|
+
if (name.startsWith("project-")) {
|
|
473
|
+
projectTopics.push(name.substring(8));
|
|
2156
474
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
reporter.info("No actions to be performed");
|
|
2160
|
-
}
|
|
2161
|
-
else {
|
|
2162
|
-
reporter.info("To be performed:");
|
|
2163
|
-
for (const change of changes) {
|
|
2164
|
-
reporter.info(` - ${JSON.stringify(change)}`);
|
|
475
|
+
if (name === "infrastructure") {
|
|
476
|
+
isInfra = true;
|
|
2165
477
|
}
|
|
478
|
+
});
|
|
479
|
+
if (projectTopics.length > 1) {
|
|
480
|
+
console.warn(`Repo ${repo.name} has multiple project groups: ${projectTopics.join(", ")}. Picking first`);
|
|
2166
481
|
}
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
if (answer.toLowerCase() === "y") {
|
|
2173
|
-
reporter.info("Executing changes");
|
|
2174
|
-
await executeChangeSet(github, changes, reporter);
|
|
2175
|
-
}
|
|
2176
|
-
else {
|
|
2177
|
-
reporter.info("Skipping");
|
|
2178
|
-
}
|
|
482
|
+
if (projectTopics.length > 0) {
|
|
483
|
+
return projectTopics[0];
|
|
484
|
+
}
|
|
485
|
+
if (isInfra) {
|
|
486
|
+
return "infrastructure";
|
|
2179
487
|
}
|
|
2180
|
-
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
function ifnull(a, other) {
|
|
491
|
+
return a === null ? other : a;
|
|
492
|
+
}
|
|
493
|
+
function getGroupedRepos(repos) {
|
|
494
|
+
return Object.values(repos.reduce((acc, repo) => {
|
|
495
|
+
const group = ifnull(getGroup(repo), "(unknown)");
|
|
496
|
+
const value = acc[group] || { name: group, items: [] };
|
|
497
|
+
return {
|
|
498
|
+
...acc,
|
|
499
|
+
[group]: {
|
|
500
|
+
...value,
|
|
501
|
+
items: [...value.items, repo],
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}, {})).sort((a, b) => a.name.localeCompare(b.name));
|
|
505
|
+
}
|
|
506
|
+
function includesTopic(repo, topic) {
|
|
507
|
+
return repo.repositoryTopics.edges.some((it) => it.node.topic.name === topic);
|
|
2181
508
|
}
|
|
2182
|
-
const command$b = {
|
|
2183
|
-
command: "configure",
|
|
2184
|
-
describe: "Configure CALS GitHub resources",
|
|
2185
|
-
builder: (yargs) => yargs
|
|
2186
|
-
.options("execute", {
|
|
2187
|
-
describe: "Execute the detected changes",
|
|
2188
|
-
type: "boolean",
|
|
2189
|
-
})
|
|
2190
|
-
.options("org", {
|
|
2191
|
-
describe: "Filter resources by GitHub organization",
|
|
2192
|
-
type: "string",
|
|
2193
|
-
})
|
|
2194
|
-
.option(definitionFileOptionName, definitionFileOptionValue),
|
|
2195
|
-
handler: async (argv) => {
|
|
2196
|
-
const reporter = createReporter(argv);
|
|
2197
|
-
const config = createConfig();
|
|
2198
|
-
const github = await createGitHubService({
|
|
2199
|
-
config,
|
|
2200
|
-
cache: createCacheProvider(config, argv),
|
|
2201
|
-
});
|
|
2202
|
-
const definition = await getDefinitionFile(argv).getDefinition();
|
|
2203
|
-
await reportRateLimit(reporter, github, async () => {
|
|
2204
|
-
const orgGetter = createOrgGetter(github);
|
|
2205
|
-
await process(reporter, github, definition, orgGetter, !!argv.execute, argv.org);
|
|
2206
|
-
});
|
|
2207
|
-
},
|
|
2208
|
-
};
|
|
2209
509
|
|
|
2210
|
-
async function generateCloneCommands({
|
|
2211
|
-
if (!opt.
|
|
510
|
+
async function generateCloneCommands({ config, github, org, ...opt }) {
|
|
511
|
+
if (!opt.all && opt.group === undefined) {
|
|
2212
512
|
yargs(hideBin(process__default.argv)).showHelp();
|
|
2213
513
|
return;
|
|
2214
514
|
}
|
|
2215
515
|
const repos = await github.getOrgRepoList({ org });
|
|
2216
516
|
const groups = getGroupedRepos(repos);
|
|
2217
|
-
|
|
2218
|
-
groups.forEach((it) => {
|
|
2219
|
-
reporter.log(it.name);
|
|
2220
|
-
});
|
|
2221
|
-
return;
|
|
2222
|
-
}
|
|
2223
|
-
groups.forEach((group) => {
|
|
517
|
+
for (const group of groups) {
|
|
2224
518
|
if (opt.group !== undefined && opt.group !== group.name) {
|
|
2225
|
-
|
|
519
|
+
continue;
|
|
2226
520
|
}
|
|
2227
521
|
group.items
|
|
2228
522
|
.filter((it) => opt.includeArchived || !it.isArchived)
|
|
2229
523
|
.filter((it) => opt.name === undefined || it.name.includes(opt.name))
|
|
2230
524
|
.filter((it) => opt.topic === undefined || includesTopic(it, opt.topic))
|
|
2231
|
-
.filter((it) => !opt.
|
|
2232
|
-
!fs.existsSync(path.resolve(config.cwd, it.name)))
|
|
525
|
+
.filter((it) => !opt.skipCloned || !fs.existsSync(path.resolve(config.cwd, it.name)))
|
|
2233
526
|
.forEach((repo) => {
|
|
2234
527
|
// The output of this is used to pipe into e.g. bash.
|
|
2235
|
-
|
|
2236
|
-
process__default.stdout.write(sprintf('[ ! -e "%s" ] && git clone %s\n', repo.name, repo.sshUrl));
|
|
528
|
+
process__default.stdout.write(`[ ! -e "${repo.name}" ] && git clone ${repo.sshUrl}\n`);
|
|
2237
529
|
});
|
|
2238
|
-
}
|
|
530
|
+
}
|
|
2239
531
|
}
|
|
2240
|
-
const command$
|
|
2241
|
-
command: "
|
|
2242
|
-
describe: "Generate
|
|
532
|
+
const command$4 = {
|
|
533
|
+
command: "clone [group]",
|
|
534
|
+
describe: "Generate git clone commands (pipe to bash to execute)",
|
|
2243
535
|
builder: (yargs) => yargs
|
|
2244
536
|
.positional("group", {
|
|
2245
|
-
describe: "
|
|
537
|
+
describe: "Clone only repos in this group",
|
|
538
|
+
type: "string",
|
|
2246
539
|
})
|
|
2247
540
|
.options("org", {
|
|
2248
|
-
|
|
2249
|
-
|
|
541
|
+
alias: "o",
|
|
542
|
+
default: "capralifecycle",
|
|
543
|
+
requiresArg: true,
|
|
544
|
+
describe: "GitHub organization",
|
|
2250
545
|
type: "string",
|
|
2251
546
|
})
|
|
2252
547
|
.option("all", {
|
|
2253
|
-
describe: "
|
|
2254
|
-
type: "boolean",
|
|
2255
|
-
})
|
|
2256
|
-
.option("list-groups", {
|
|
2257
|
-
alias: "l",
|
|
2258
|
-
describe: "List available groups",
|
|
548
|
+
describe: "Clone all repos",
|
|
2259
549
|
type: "boolean",
|
|
2260
550
|
})
|
|
2261
551
|
.option("include-archived", {
|
|
@@ -2266,90 +556,58 @@ const command$a = {
|
|
|
2266
556
|
.option("name", {
|
|
2267
557
|
describe: "Filter to include the specified name",
|
|
2268
558
|
type: "string",
|
|
559
|
+
requiresArg: true,
|
|
2269
560
|
})
|
|
2270
561
|
.option("topic", {
|
|
2271
562
|
alias: "t",
|
|
2272
563
|
describe: "Filter by specific topic",
|
|
2273
564
|
type: "string",
|
|
565
|
+
requiresArg: true,
|
|
2274
566
|
})
|
|
2275
|
-
.option("
|
|
2276
|
-
alias: "
|
|
2277
|
-
describe: "
|
|
567
|
+
.option("skip-cloned", {
|
|
568
|
+
alias: "s",
|
|
569
|
+
describe: "Skip repos already cloned in working directory",
|
|
2278
570
|
type: "boolean",
|
|
2279
571
|
}),
|
|
2280
572
|
handler: async (argv) => {
|
|
2281
573
|
const config = createConfig();
|
|
2282
574
|
return generateCloneCommands({
|
|
2283
|
-
reporter: createReporter(argv),
|
|
2284
575
|
config,
|
|
2285
576
|
github: await createGitHubService({
|
|
2286
|
-
config,
|
|
2287
577
|
cache: createCacheProvider(config, argv),
|
|
2288
578
|
}),
|
|
2289
579
|
all: !!argv.all,
|
|
2290
|
-
listGroups: !!argv["list-groups"],
|
|
2291
580
|
includeArchived: !!argv["include-archived"],
|
|
2292
581
|
name: argv.name,
|
|
2293
582
|
topic: argv.topic,
|
|
2294
|
-
|
|
583
|
+
skipCloned: !!argv["skip-cloned"],
|
|
2295
584
|
group: argv.group,
|
|
2296
585
|
org: argv.org,
|
|
2297
586
|
});
|
|
2298
587
|
},
|
|
2299
588
|
};
|
|
2300
589
|
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
// Cheat by mutating.
|
|
2312
|
-
let t = acc.find((it) => it.key === key);
|
|
2313
|
-
if (t === undefined) {
|
|
2314
|
-
t = {
|
|
2315
|
-
key,
|
|
2316
|
-
old: [],
|
|
2317
|
-
oldSnyk: [],
|
|
2318
|
-
recent: [],
|
|
2319
|
-
recentSnyk: [],
|
|
2320
|
-
};
|
|
2321
|
-
acc.push(t);
|
|
2322
|
-
}
|
|
2323
|
-
t[snyk ? (old ? "oldSnyk" : "recentSnyk") : old ? "old" : "recent"].push(cur);
|
|
2324
|
-
return acc;
|
|
2325
|
-
}, [])
|
|
2326
|
-
.sort((a, b) => a.key.localeCompare(b.key));
|
|
2327
|
-
if (categories.length === 0) {
|
|
2328
|
-
reporter.log("No pull requests found");
|
|
2329
|
-
}
|
|
2330
|
-
else {
|
|
2331
|
-
reporter.log("Pull requests stats:");
|
|
2332
|
-
reporter.log("A pull request is considered old after 60 days");
|
|
2333
|
-
reporter.log("");
|
|
2334
|
-
reporter.log(sprintf("%-40s %12s %2s %12s %2s", "", "normal", "", "snyk", ""));
|
|
2335
|
-
reporter.log(sprintf("%-40s %7s %7s %7s %7s", "Repo", "old", "recent", "old", "recent"));
|
|
2336
|
-
categories.forEach((cat) => {
|
|
2337
|
-
reporter.log(sprintf("%-40s %7s %7s %7s %7s", cat.key, cat.old.length === 0 ? "" : cat.old.length, cat.recent.length === 0 ? "" : cat.recent.length, cat.oldSnyk.length === 0 ? "" : cat.oldSnyk.length, cat.recentSnyk.length === 0 ? "" : cat.recentSnyk.length));
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
const command$9 = {
|
|
2342
|
-
command: "list-pull-requests-stats",
|
|
2343
|
-
describe: "List stats for pull requests with special filter",
|
|
590
|
+
const command$3 = {
|
|
591
|
+
command: "groups",
|
|
592
|
+
describe: "List available repository groups in a GitHub organization",
|
|
593
|
+
builder: (yargs) => yargs.options("org", {
|
|
594
|
+
alias: "o",
|
|
595
|
+
default: "capralifecycle",
|
|
596
|
+
requiresArg: true,
|
|
597
|
+
describe: "GitHub organization",
|
|
598
|
+
type: "string",
|
|
599
|
+
}),
|
|
2344
600
|
handler: async (argv) => {
|
|
2345
601
|
const config = createConfig();
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
config,
|
|
2350
|
-
cache: createCacheProvider(config, argv),
|
|
2351
|
-
}),
|
|
602
|
+
const reporter = createReporter();
|
|
603
|
+
const github = await createGitHubService({
|
|
604
|
+
cache: createCacheProvider(config, argv),
|
|
2352
605
|
});
|
|
606
|
+
const repos = await github.getOrgRepoList({ org: argv.org });
|
|
607
|
+
const groups = getGroupedRepos(repos);
|
|
608
|
+
for (const group of groups) {
|
|
609
|
+
reporter.log(group.name);
|
|
610
|
+
}
|
|
2353
611
|
},
|
|
2354
612
|
};
|
|
2355
613
|
|
|
@@ -2383,181 +641,268 @@ async function listRepos({ reporter, github, includeArchived, name = undefined,
|
|
|
2383
641
|
if (!csv && compact) {
|
|
2384
642
|
reporter.log(`${group.name}`);
|
|
2385
643
|
}
|
|
2386
|
-
else if (!csv) {
|
|
2387
|
-
reporter.log("");
|
|
2388
|
-
reporter.log(`======== ${group.name} ========`);
|
|
644
|
+
else if (!csv) {
|
|
645
|
+
reporter.log("");
|
|
646
|
+
reporter.log(`======== ${group.name} ========`);
|
|
647
|
+
}
|
|
648
|
+
group.items.forEach((repo) => {
|
|
649
|
+
if (csv) {
|
|
650
|
+
// We assume we have no repos or group names with a comma in its name.
|
|
651
|
+
process__default.stdout.write(`${repo.name},${group.name}\n`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (compact) {
|
|
655
|
+
reporter.log(`- ${repo.name}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
reporter.log(`${repo.name}`);
|
|
659
|
+
reporter.log(`- Created: ${repo.createdAt}`);
|
|
660
|
+
reporter.log(`- Updated: ${repo.updatedAt}`);
|
|
661
|
+
if (repo.repositoryTopics.edges.length === 0) {
|
|
662
|
+
reporter.log("- Topics: (none)");
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
reporter.log("- Topics:");
|
|
666
|
+
repo.repositoryTopics.edges.forEach((edge) => {
|
|
667
|
+
reporter.log(` - ${edge.node.topic.name}`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
if (csv) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
reporter.log("");
|
|
676
|
+
reporter.log(`Total number of repos: ${repos.length}`);
|
|
677
|
+
const missingGroup = getReposMissingGroup(repos);
|
|
678
|
+
if (missingGroup.length > 0) {
|
|
679
|
+
reporter.log("");
|
|
680
|
+
reporter.log("Repos missing group/customer topic:");
|
|
681
|
+
missingGroup.forEach((repo) => {
|
|
682
|
+
reporter.log(`- ${repo.name}`);
|
|
683
|
+
});
|
|
684
|
+
reporter.log("Useful search query: https://github.com/capralifecycle?q=topics%3A0");
|
|
685
|
+
}
|
|
686
|
+
const days = 180;
|
|
687
|
+
const oldRepos = getOldRepos(repos, days);
|
|
688
|
+
if (oldRepos.length > 0) {
|
|
689
|
+
reporter.log("");
|
|
690
|
+
reporter.log(`Repositories not updated for ${days} days:`);
|
|
691
|
+
oldRepos.forEach((repo) => {
|
|
692
|
+
reporter.log(`- ${repo.name} - ${repo.updatedAt}`);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const command$2 = {
|
|
697
|
+
command: "repos",
|
|
698
|
+
describe: "List repositories in a GitHub organization",
|
|
699
|
+
builder: (yargs) => yargs
|
|
700
|
+
.options("org", {
|
|
701
|
+
alias: "o",
|
|
702
|
+
default: "capralifecycle",
|
|
703
|
+
requiresArg: true,
|
|
704
|
+
describe: "GitHub organization",
|
|
705
|
+
type: "string",
|
|
706
|
+
})
|
|
707
|
+
.option("include-archived", {
|
|
708
|
+
alias: "a",
|
|
709
|
+
describe: "Include archived repos",
|
|
710
|
+
type: "boolean",
|
|
711
|
+
})
|
|
712
|
+
.options("compact", {
|
|
713
|
+
alias: "c",
|
|
714
|
+
describe: "Compact output list",
|
|
715
|
+
type: "boolean",
|
|
716
|
+
})
|
|
717
|
+
.options("csv", {
|
|
718
|
+
describe: "Output as a CSV list that can be used for automation",
|
|
719
|
+
type: "boolean",
|
|
720
|
+
})
|
|
721
|
+
.option("name", {
|
|
722
|
+
describe: "Filter to include the specified name",
|
|
723
|
+
type: "string",
|
|
724
|
+
requiresArg: true,
|
|
725
|
+
})
|
|
726
|
+
.option("topic", {
|
|
727
|
+
alias: "t",
|
|
728
|
+
describe: "Filter by specific topic",
|
|
729
|
+
type: "string",
|
|
730
|
+
requiresArg: true,
|
|
731
|
+
}),
|
|
732
|
+
handler: async (argv) => {
|
|
733
|
+
const config = createConfig();
|
|
734
|
+
await listRepos({
|
|
735
|
+
reporter: createReporter(),
|
|
736
|
+
github: await createGitHubService({
|
|
737
|
+
cache: createCacheProvider(config, argv),
|
|
738
|
+
}),
|
|
739
|
+
includeArchived: !!argv["include-archived"],
|
|
740
|
+
name: argv.name,
|
|
741
|
+
topic: argv.topic,
|
|
742
|
+
compact: !!argv.compact,
|
|
743
|
+
csv: !!argv.csv,
|
|
744
|
+
org: argv.org,
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
var type = "object";
|
|
750
|
+
var properties = {
|
|
751
|
+
projects: {
|
|
752
|
+
type: "array",
|
|
753
|
+
items: {
|
|
754
|
+
$ref: "#/definitions/Project"
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
var required = [
|
|
759
|
+
"projects"
|
|
760
|
+
];
|
|
761
|
+
var definitions = {
|
|
762
|
+
Project: {
|
|
763
|
+
type: "object",
|
|
764
|
+
properties: {
|
|
765
|
+
name: {
|
|
766
|
+
type: "string"
|
|
767
|
+
},
|
|
768
|
+
github: {
|
|
769
|
+
type: "array",
|
|
770
|
+
items: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
organization: {
|
|
774
|
+
type: "string"
|
|
775
|
+
},
|
|
776
|
+
repos: {
|
|
777
|
+
type: "array",
|
|
778
|
+
items: {
|
|
779
|
+
$ref: "#/definitions/DefinitionRepo"
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
required: [
|
|
784
|
+
"organization"
|
|
785
|
+
]
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
tags: {
|
|
789
|
+
type: "array",
|
|
790
|
+
items: {
|
|
791
|
+
type: "string"
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
required: [
|
|
796
|
+
"github",
|
|
797
|
+
"name"
|
|
798
|
+
]
|
|
799
|
+
},
|
|
800
|
+
DefinitionRepo: {
|
|
801
|
+
type: "object",
|
|
802
|
+
properties: {
|
|
803
|
+
name: {
|
|
804
|
+
type: "string"
|
|
805
|
+
},
|
|
806
|
+
previousNames: {
|
|
807
|
+
type: "array",
|
|
808
|
+
items: {
|
|
809
|
+
$ref: "#/definitions/DefinitionRepoPreviousName"
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
archived: {
|
|
813
|
+
type: "boolean"
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
required: [
|
|
817
|
+
"name"
|
|
818
|
+
]
|
|
819
|
+
},
|
|
820
|
+
DefinitionRepoPreviousName: {
|
|
821
|
+
type: "object",
|
|
822
|
+
properties: {
|
|
823
|
+
name: {
|
|
824
|
+
type: "string"
|
|
825
|
+
},
|
|
826
|
+
project: {
|
|
827
|
+
type: "string"
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
required: [
|
|
831
|
+
"name",
|
|
832
|
+
"project"
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var $schema = "http://json-schema.org/draft-07/schema#";
|
|
837
|
+
var schema = {
|
|
838
|
+
type: type,
|
|
839
|
+
properties: properties,
|
|
840
|
+
required: required,
|
|
841
|
+
definitions: definitions,
|
|
842
|
+
$schema: $schema
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
function getRepoId(orgName, repoName) {
|
|
846
|
+
return `${orgName}/${repoName}`;
|
|
847
|
+
}
|
|
848
|
+
function checkAgainstSchema(value) {
|
|
849
|
+
const ajv = new AJV({ allErrors: true });
|
|
850
|
+
const valid = ajv.validate(schema, value);
|
|
851
|
+
return valid
|
|
852
|
+
? { definition: value }
|
|
853
|
+
: { error: ajv.errorsText() ?? "Unknown error" };
|
|
854
|
+
}
|
|
855
|
+
function requireValidDefinition(definition) {
|
|
856
|
+
// Verify no duplicates in project names.
|
|
857
|
+
definition.projects.reduce((acc, project) => {
|
|
858
|
+
if (acc.includes(project.name)) {
|
|
859
|
+
throw new Error(`Duplicate project: ${project.name}`);
|
|
860
|
+
}
|
|
861
|
+
return [...acc, project.name];
|
|
862
|
+
}, []);
|
|
863
|
+
// Verify no duplicates in repos.
|
|
864
|
+
definition.projects
|
|
865
|
+
.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => getRepoId(org.organization, repo.name))))
|
|
866
|
+
.reduce((acc, repoName) => {
|
|
867
|
+
if (acc.includes(repoName)) {
|
|
868
|
+
throw new Error(`Duplicate repo: ${repoName}`);
|
|
2389
869
|
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
reporter.log(`- ${repo.name}`);
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
reporter.log(`${repo.name}`);
|
|
2401
|
-
reporter.log(`- Created: ${repo.createdAt}`);
|
|
2402
|
-
reporter.log(`- Updated: ${repo.updatedAt}`);
|
|
2403
|
-
if (repo.repositoryTopics.edges.length === 0) {
|
|
2404
|
-
reporter.log("- Topics: (none)");
|
|
2405
|
-
}
|
|
2406
|
-
else {
|
|
2407
|
-
reporter.log("- Topics:");
|
|
2408
|
-
repo.repositoryTopics.edges.forEach((edge) => {
|
|
2409
|
-
reporter.log(` - ${edge.node.topic.name}`);
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
});
|
|
2413
|
-
});
|
|
2414
|
-
if (csv) {
|
|
2415
|
-
return;
|
|
870
|
+
return [...acc, repoName];
|
|
871
|
+
}, []);
|
|
872
|
+
}
|
|
873
|
+
class DefinitionFile {
|
|
874
|
+
path;
|
|
875
|
+
constructor(path) {
|
|
876
|
+
this.path = path;
|
|
2416
877
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
reporter.log(`- ${repo.name}`);
|
|
2425
|
-
});
|
|
2426
|
-
reporter.log("Useful search query: https://github.com/capralifecycle?q=topics%3A0");
|
|
878
|
+
async getContents() {
|
|
879
|
+
return new Promise((resolve, reject) => fs.readFile(this.path, "utf-8", (err, data) => {
|
|
880
|
+
if (err)
|
|
881
|
+
reject(err);
|
|
882
|
+
else
|
|
883
|
+
resolve(data);
|
|
884
|
+
}));
|
|
2427
885
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
if (oldRepos.length > 0) {
|
|
2431
|
-
reporter.log("");
|
|
2432
|
-
reporter.log(`Repositories not updated for ${days} days:`);
|
|
2433
|
-
oldRepos.forEach((repo) => {
|
|
2434
|
-
reporter.log(`- ${repo.name} - ${repo.updatedAt}`);
|
|
2435
|
-
});
|
|
886
|
+
async getDefinition() {
|
|
887
|
+
return parseDefinition(await this.getContents());
|
|
2436
888
|
}
|
|
2437
889
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
.options("org", {
|
|
2443
|
-
required: true,
|
|
2444
|
-
describe: "Specify GitHub organization",
|
|
2445
|
-
type: "string",
|
|
2446
|
-
})
|
|
2447
|
-
.option("include-archived", {
|
|
2448
|
-
alias: "a",
|
|
2449
|
-
describe: "Include archived repos",
|
|
2450
|
-
type: "boolean",
|
|
2451
|
-
})
|
|
2452
|
-
.options("compact", {
|
|
2453
|
-
alias: "c",
|
|
2454
|
-
describe: "Compact output list",
|
|
2455
|
-
type: "boolean",
|
|
2456
|
-
})
|
|
2457
|
-
.options("csv", {
|
|
2458
|
-
describe: "Output as a CSV list that can be used for automation",
|
|
2459
|
-
type: "boolean",
|
|
2460
|
-
})
|
|
2461
|
-
.option("name", {
|
|
2462
|
-
describe: "Filter to include the specified name",
|
|
2463
|
-
type: "string",
|
|
2464
|
-
})
|
|
2465
|
-
.option("topic", {
|
|
2466
|
-
alias: "t",
|
|
2467
|
-
describe: "Filter by specific topic",
|
|
2468
|
-
type: "string",
|
|
2469
|
-
}),
|
|
2470
|
-
handler: async (argv) => {
|
|
2471
|
-
const config = createConfig();
|
|
2472
|
-
await listRepos({
|
|
2473
|
-
reporter: createReporter(argv),
|
|
2474
|
-
github: await createGitHubService({
|
|
2475
|
-
config,
|
|
2476
|
-
cache: createCacheProvider(config, argv),
|
|
2477
|
-
}),
|
|
2478
|
-
includeArchived: !!argv["include-archived"],
|
|
2479
|
-
name: argv.name,
|
|
2480
|
-
topic: argv.topic,
|
|
2481
|
-
compact: !!argv.compact,
|
|
2482
|
-
csv: !!argv.csv,
|
|
2483
|
-
org: argv.org,
|
|
2484
|
-
});
|
|
2485
|
-
},
|
|
2486
|
-
};
|
|
2487
|
-
|
|
2488
|
-
const e = encodeURIComponent;
|
|
2489
|
-
async function listWebhooks(reporter, _cache, github, org) {
|
|
2490
|
-
const repos = (await github.getOrgRepoList({ org })).filter((it) => !it.isArchived);
|
|
2491
|
-
for (const repo of repos) {
|
|
2492
|
-
reporter.log("");
|
|
2493
|
-
reporter.log(`${repo.name}: https://github.com/capralifecycle/${e(repo.name)}/settings/hooks`);
|
|
2494
|
-
const hooks = await github.getRepositoryHooks(repo.owner.login, repo.name);
|
|
2495
|
-
for (const hook of hooks) {
|
|
2496
|
-
if (hook.config.url === undefined ||
|
|
2497
|
-
!hook.config.url.includes("jenkins")) {
|
|
2498
|
-
continue;
|
|
2499
|
-
}
|
|
2500
|
-
switch (hook.name) {
|
|
2501
|
-
case "web":
|
|
2502
|
-
reporter.log(sprintf(" web: %s (%s) (%s)", hook.config.url, hook.last_response.code, hook.events.join(", ")));
|
|
2503
|
-
break;
|
|
2504
|
-
case "jenkinsgit":
|
|
2505
|
-
reporter.log(sprintf(" jenkinsgit: %s (%s) (%s)",
|
|
2506
|
-
// This is undocumented.
|
|
2507
|
-
hook.config.jenkins_url, hook.last_response.code, hook.events.join(", ")));
|
|
2508
|
-
break;
|
|
2509
|
-
case "docker":
|
|
2510
|
-
reporter.log(sprintf(" docker (%s) (%s)", hook.last_response.code, hook.events.join(", ")));
|
|
2511
|
-
break;
|
|
2512
|
-
default:
|
|
2513
|
-
reporter.log(` ${hook.name}: <unknown type>`);
|
|
2514
|
-
reporter.log(JSON.stringify(hook));
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
890
|
+
function parseDefinition(value) {
|
|
891
|
+
const result = checkAgainstSchema(yaml.load(value));
|
|
892
|
+
if ("error" in result) {
|
|
893
|
+
throw new Error(`Definition content invalid: ${result.error}`);
|
|
2517
894
|
}
|
|
895
|
+
requireValidDefinition(result.definition);
|
|
896
|
+
return result.definition;
|
|
2518
897
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
}),
|
|
2527
|
-
handler: async (argv) => {
|
|
2528
|
-
const config = createConfig();
|
|
2529
|
-
const cacheProvider = createCacheProvider(config, argv);
|
|
2530
|
-
await listWebhooks(createReporter(argv), cacheProvider, await createGitHubService({ config, cache: cacheProvider }), argv.org);
|
|
2531
|
-
},
|
|
2532
|
-
};
|
|
2533
|
-
|
|
2534
|
-
async function setToken$1({ reporter, token, tokenProvider, }) {
|
|
2535
|
-
if (token === undefined) {
|
|
2536
|
-
reporter.info("Need API token to talk to GitHub");
|
|
2537
|
-
reporter.info("https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook");
|
|
2538
|
-
const inputToken = await read({
|
|
2539
|
-
prompt: "Enter new GitHub API token: ",
|
|
2540
|
-
silent: true,
|
|
2541
|
-
});
|
|
2542
|
-
token = inputToken;
|
|
2543
|
-
}
|
|
2544
|
-
await tokenProvider.setToken(token);
|
|
2545
|
-
reporter.info("Token saved");
|
|
898
|
+
function getRepos(definition) {
|
|
899
|
+
return definition.projects.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => ({
|
|
900
|
+
id: getRepoId(org.organization, repo.name),
|
|
901
|
+
orgName: org.organization,
|
|
902
|
+
project,
|
|
903
|
+
repo,
|
|
904
|
+
}))));
|
|
2546
905
|
}
|
|
2547
|
-
const command$6 = {
|
|
2548
|
-
command: "set-token",
|
|
2549
|
-
describe: "Set GitHub token for API calls",
|
|
2550
|
-
builder: (yargs) => yargs.positional("token", {
|
|
2551
|
-
describe: "Token. If not provided it will be requested as input. Can be generated at https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook",
|
|
2552
|
-
}),
|
|
2553
|
-
handler: async (argv) => {
|
|
2554
|
-
await setToken$1({
|
|
2555
|
-
reporter: createReporter(argv),
|
|
2556
|
-
token: argv.token,
|
|
2557
|
-
tokenProvider: new GitHubTokenCliProvider(),
|
|
2558
|
-
});
|
|
2559
|
-
},
|
|
2560
|
-
};
|
|
2561
906
|
|
|
2562
907
|
function wasUpdated(output) {
|
|
2563
908
|
return output.startsWith("Updating ");
|
|
@@ -2887,7 +1232,7 @@ async function getExpectedRepos(reporter, github, cals, rootdir) {
|
|
|
2887
1232
|
};
|
|
2888
1233
|
}
|
|
2889
1234
|
async function getInput(prompt) {
|
|
2890
|
-
return
|
|
1235
|
+
return readInput({
|
|
2891
1236
|
prompt,
|
|
2892
1237
|
timeout: 60000,
|
|
2893
1238
|
});
|
|
@@ -2912,7 +1257,7 @@ async function askMoveConfirm() {
|
|
|
2912
1257
|
return false;
|
|
2913
1258
|
}
|
|
2914
1259
|
}
|
|
2915
|
-
async function sync
|
|
1260
|
+
async function sync({ reporter, github, cals, rootdir, clone, move, }) {
|
|
2916
1261
|
const { expectedRepos, definitionRepo } = await getExpectedRepos(reporter, github, cals, rootdir);
|
|
2917
1262
|
const unknownDirs = [];
|
|
2918
1263
|
const foundRepos = [];
|
|
@@ -2972,8 +1317,8 @@ async function sync$1({ reporter, github, cals, rootdir, askClone, askMove, }) {
|
|
|
2972
1317
|
for (const it of movedRepos) {
|
|
2973
1318
|
reporter.info(` ${it.actualRelpath} -> ${getRelpath(it)}`);
|
|
2974
1319
|
}
|
|
2975
|
-
if (!
|
|
2976
|
-
reporter.info("To move these repos on disk add --
|
|
1320
|
+
if (!move) {
|
|
1321
|
+
reporter.info("To move these repos on disk add --move option");
|
|
2977
1322
|
}
|
|
2978
1323
|
else {
|
|
2979
1324
|
const shouldMove = await askMoveConfirm();
|
|
@@ -3005,8 +1350,8 @@ async function sync$1({ reporter, github, cals, rootdir, askClone, askMove, }) {
|
|
|
3005
1350
|
for (const it of missingRepos) {
|
|
3006
1351
|
reporter.info(` ${it.id}`);
|
|
3007
1352
|
}
|
|
3008
|
-
if (!
|
|
3009
|
-
reporter.info("To clone these repos add --
|
|
1353
|
+
if (!clone) {
|
|
1354
|
+
reporter.info("To clone these repos add --clone option");
|
|
3010
1355
|
}
|
|
3011
1356
|
else {
|
|
3012
1357
|
reporter.info("You must already have working credentials for GitHub set up for clone to work");
|
|
@@ -3050,20 +1395,21 @@ async function loadCalsManifest(config, reporter) {
|
|
|
3050
1395
|
cals,
|
|
3051
1396
|
};
|
|
3052
1397
|
}
|
|
3053
|
-
const command$
|
|
1398
|
+
const command$1 = {
|
|
3054
1399
|
command: "sync",
|
|
3055
1400
|
describe: "Sync repositories for working directory",
|
|
3056
1401
|
builder: (yargs) => yargs
|
|
3057
|
-
.option("
|
|
1402
|
+
.option("clone", {
|
|
3058
1403
|
alias: "c",
|
|
3059
|
-
describe: "
|
|
1404
|
+
describe: "Prompt to clone missing repos",
|
|
3060
1405
|
type: "boolean",
|
|
3061
1406
|
})
|
|
3062
|
-
.option("
|
|
3063
|
-
|
|
1407
|
+
.option("move", {
|
|
1408
|
+
alias: "m",
|
|
1409
|
+
describe: "Prompt to move renamed repos",
|
|
3064
1410
|
type: "boolean",
|
|
3065
1411
|
})
|
|
3066
|
-
.usage(`cals
|
|
1412
|
+
.usage(`cals sync
|
|
3067
1413
|
|
|
3068
1414
|
Synchronize all checked out GitHub repositories within the working directory
|
|
3069
1415
|
grouped by the project in the resource definition file. The command can also
|
|
@@ -3100,238 +1446,109 @@ will be stored there.`),
|
|
|
3100
1446
|
handler: async (argv) => {
|
|
3101
1447
|
const config = createConfig();
|
|
3102
1448
|
const github = await createGitHubService({
|
|
3103
|
-
config,
|
|
3104
1449
|
cache: createCacheProvider(config, argv),
|
|
3105
1450
|
});
|
|
3106
|
-
const reporter = createReporter(
|
|
1451
|
+
const reporter = createReporter();
|
|
3107
1452
|
const manifest = await loadCalsManifest(config, reporter);
|
|
3108
1453
|
if (manifest === null)
|
|
3109
1454
|
return;
|
|
3110
1455
|
const { dir, cals } = manifest;
|
|
3111
|
-
return sync
|
|
1456
|
+
return sync({
|
|
3112
1457
|
reporter,
|
|
3113
1458
|
github,
|
|
3114
1459
|
cals,
|
|
3115
1460
|
rootdir: dir,
|
|
3116
|
-
|
|
3117
|
-
|
|
1461
|
+
clone: !!argv.clone,
|
|
1462
|
+
move: !!argv.move,
|
|
3118
1463
|
});
|
|
3119
1464
|
},
|
|
3120
1465
|
};
|
|
3121
1466
|
|
|
3122
|
-
const command
|
|
3123
|
-
command: "
|
|
3124
|
-
describe: "
|
|
3125
|
-
builder: (yargs) => yargs
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
.command(command$7)
|
|
3132
|
-
.command(command$6)
|
|
3133
|
-
.command(command$5)
|
|
3134
|
-
.demandCommand()
|
|
3135
|
-
.usage(`cals github
|
|
3136
|
-
|
|
3137
|
-
Notes:
|
|
3138
|
-
Before doing anything against GitHub you need to configure a token
|
|
3139
|
-
used for authentication. The following command will ask for a token
|
|
3140
|
-
and provide a link to generate one:
|
|
3141
|
-
$ cals github set-token
|
|
3142
|
-
|
|
3143
|
-
Quick clone all repos:
|
|
3144
|
-
$ cals github generate-clone-commands --org capralifecycle --all -x | bash
|
|
3145
|
-
|
|
3146
|
-
And for a specific project:
|
|
3147
|
-
$ cals github generate-clone-commands --org capralifecycle -x buildtools | bash
|
|
3148
|
-
|
|
3149
|
-
Keeping up to date with removed/renamed repos:
|
|
3150
|
-
$ cals github analyze-directory --org capralifecycle
|
|
3151
|
-
|
|
3152
|
-
Some responses are cached for some time. Use the --validate-cache
|
|
3153
|
-
option to avoid stale cache. The cache can also be cleared with
|
|
3154
|
-
the "cals delete-cache" command.`),
|
|
3155
|
-
handler: () => {
|
|
3156
|
-
yargs(hideBin(process__default.argv)).showHelp();
|
|
3157
|
-
},
|
|
3158
|
-
};
|
|
3159
|
-
|
|
3160
|
-
function totalSeverityCount(project) {
|
|
3161
|
-
return ((project.issueCountsBySeverity.critical ?? 0) +
|
|
3162
|
-
project.issueCountsBySeverity.high +
|
|
3163
|
-
project.issueCountsBySeverity.medium +
|
|
3164
|
-
project.issueCountsBySeverity.low);
|
|
3165
|
-
}
|
|
3166
|
-
function buildStatsLine(stats) {
|
|
3167
|
-
function item(num, str) {
|
|
3168
|
-
return num === 0 ? " ".repeat(str.length + 4) : sprintf("%3d %s", num, str);
|
|
3169
|
-
}
|
|
3170
|
-
return sprintf("%s %s %s %s", item(stats.critical ?? 0, "critical"), item(stats.high, "high"), item(stats.medium, "medium"), item(stats.low, "low"));
|
|
3171
|
-
}
|
|
3172
|
-
async function report({ reporter, snyk, definitionFile, }) {
|
|
3173
|
-
const definition = await definitionFile.getDefinition();
|
|
3174
|
-
const reposWithIssues = (await snyk.getProjects(definition)).filter((it) => totalSeverityCount(it) > 0);
|
|
3175
|
-
const definitionRepos = getRepos(definition);
|
|
3176
|
-
function getProject(p) {
|
|
3177
|
-
const id = getGitHubRepoId(getGitHubRepo(p));
|
|
3178
|
-
const def = id === undefined ? undefined : definitionRepos.find((it) => it.id === id);
|
|
3179
|
-
return def === undefined ? undefined : def.project;
|
|
3180
|
-
}
|
|
3181
|
-
const enhancedRepos = reposWithIssues.map((repo) => ({
|
|
3182
|
-
repo,
|
|
3183
|
-
project: getProject(repo),
|
|
3184
|
-
}));
|
|
3185
|
-
function getProjectName(project) {
|
|
3186
|
-
return project ? project.name : "unknown project";
|
|
3187
|
-
}
|
|
3188
|
-
const byProjects = sortBy(Object.values(groupBy(enhancedRepos, (it) => it.project ? it.project.name : "unknown")), (it) => getProjectName(it[0].project));
|
|
3189
|
-
if (byProjects.length === 0) {
|
|
3190
|
-
reporter.info("No issues found");
|
|
3191
|
-
}
|
|
3192
|
-
else {
|
|
3193
|
-
reporter.info(sprintf("%-70s %s", "Total count", buildStatsLine({
|
|
3194
|
-
critical: sumBy(reposWithIssues, (it) => it.issueCountsBySeverity.critical ?? 0),
|
|
3195
|
-
high: sumBy(reposWithIssues, (it) => it.issueCountsBySeverity.high),
|
|
3196
|
-
medium: sumBy(reposWithIssues, (it) => it.issueCountsBySeverity.medium),
|
|
3197
|
-
low: sumBy(reposWithIssues, (it) => it.issueCountsBySeverity.low),
|
|
3198
|
-
})));
|
|
3199
|
-
reporter.info("Issues by project:");
|
|
3200
|
-
byProjects.forEach((repos) => {
|
|
3201
|
-
const project = repos[0].project;
|
|
3202
|
-
const totalCount = {
|
|
3203
|
-
critical: sumBy(repos, (it) => it.repo.issueCountsBySeverity.critical ?? 0),
|
|
3204
|
-
high: sumBy(repos, (it) => it.repo.issueCountsBySeverity.high),
|
|
3205
|
-
medium: sumBy(repos, (it) => it.repo.issueCountsBySeverity.medium),
|
|
3206
|
-
low: sumBy(repos, (it) => it.repo.issueCountsBySeverity.low),
|
|
3207
|
-
};
|
|
3208
|
-
reporter.info("");
|
|
3209
|
-
reporter.info(sprintf("%-70s %s", getProjectName(project), buildStatsLine(totalCount)));
|
|
3210
|
-
for (const { repo } of repos) {
|
|
3211
|
-
reporter.info(sprintf(" %-68s %s", repo.name, buildStatsLine(repo.issueCountsBySeverity)));
|
|
3212
|
-
}
|
|
3213
|
-
});
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
const command$3 = {
|
|
3217
|
-
command: "report",
|
|
3218
|
-
describe: "Report Snyk projects status",
|
|
3219
|
-
builder: (yargs) => yargs.option(definitionFileOptionName, definitionFileOptionValue),
|
|
3220
|
-
handler: async (argv) => report({
|
|
3221
|
-
reporter: createReporter(argv),
|
|
3222
|
-
snyk: createSnykService({ config: createConfig() }),
|
|
3223
|
-
definitionFile: getDefinitionFile(argv),
|
|
1467
|
+
const command = {
|
|
1468
|
+
command: "topics",
|
|
1469
|
+
describe: "List customer topics in a GitHub organization",
|
|
1470
|
+
builder: (yargs) => yargs.options("org", {
|
|
1471
|
+
alias: "o",
|
|
1472
|
+
default: "capralifecycle",
|
|
1473
|
+
requiresArg: true,
|
|
1474
|
+
describe: "GitHub organization",
|
|
1475
|
+
type: "string",
|
|
3224
1476
|
}),
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
reporter.info("See https://app.snyk.io/account");
|
|
3231
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
3232
|
-
const inputToken = await read({
|
|
3233
|
-
prompt: "Enter new Snyk API token: ",
|
|
3234
|
-
silent: true,
|
|
1477
|
+
handler: async (argv) => {
|
|
1478
|
+
const config = createConfig();
|
|
1479
|
+
const reporter = createReporter();
|
|
1480
|
+
const github = await createGitHubService({
|
|
1481
|
+
cache: createCacheProvider(config, argv),
|
|
3235
1482
|
});
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
}),
|
|
1483
|
+
const repos = await github.getOrgRepoList({ org: argv.org });
|
|
1484
|
+
const topics = new Set();
|
|
1485
|
+
for (const repo of repos) {
|
|
1486
|
+
for (const edge of repo.repositoryTopics.edges) {
|
|
1487
|
+
const name = edge.node.topic.name;
|
|
1488
|
+
if (name.startsWith("customer-")) {
|
|
1489
|
+
topics.add(name);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const sorted = [...topics].sort((a, b) => a.localeCompare(b));
|
|
1494
|
+
for (const topic of sorted) {
|
|
1495
|
+
reporter.log(topic);
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
3252
1498
|
};
|
|
3253
1499
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
.
|
|
3258
|
-
.
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
const
|
|
3262
|
-
const
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
reporter.info(`Not in Snyk: ${it.project.name} / ${it.repo.name}`);
|
|
3269
|
-
});
|
|
3270
|
-
extraInSnyk.forEach((it) => {
|
|
3271
|
-
reporter.info(`Should not be in Snyk? ${it.owner}/${it.name}`);
|
|
3272
|
-
});
|
|
1500
|
+
function parseVersion(v) {
|
|
1501
|
+
return v
|
|
1502
|
+
.replace(/^[^\d]*/, "")
|
|
1503
|
+
.split(".")
|
|
1504
|
+
.map(Number);
|
|
1505
|
+
}
|
|
1506
|
+
function satisfiesMinVersion(current, required) {
|
|
1507
|
+
const cur = parseVersion(current);
|
|
1508
|
+
const req = parseVersion(required.replace(/^>=?\s*/, ""));
|
|
1509
|
+
for (let i = 0; i < 3; i++) {
|
|
1510
|
+
if ((cur[i] ?? 0) > (req[i] ?? 0))
|
|
1511
|
+
return true;
|
|
1512
|
+
if ((cur[i] ?? 0) < (req[i] ?? 0))
|
|
1513
|
+
return false;
|
|
3273
1514
|
}
|
|
1515
|
+
return true;
|
|
3274
1516
|
}
|
|
3275
|
-
const command$1 = {
|
|
3276
|
-
command: "sync",
|
|
3277
|
-
describe: "Sync Snyk projects (currently only reports, no automation)",
|
|
3278
|
-
builder: (yargs) => yargs.option(definitionFileOptionName, definitionFileOptionValue),
|
|
3279
|
-
handler: async (argv) => sync({
|
|
3280
|
-
reporter: createReporter(argv),
|
|
3281
|
-
snyk: createSnykService({ config: createConfig() }),
|
|
3282
|
-
definitionFile: getDefinitionFile(argv),
|
|
3283
|
-
}),
|
|
3284
|
-
};
|
|
3285
|
-
|
|
3286
|
-
const command = {
|
|
3287
|
-
command: "snyk",
|
|
3288
|
-
describe: "Integration with Snyk",
|
|
3289
|
-
builder: (yargs) => yargs
|
|
3290
|
-
.command(command$2)
|
|
3291
|
-
.command(command$3)
|
|
3292
|
-
.command(command$1)
|
|
3293
|
-
.demandCommand()
|
|
3294
|
-
.usage(`cals snyk
|
|
3295
|
-
|
|
3296
|
-
Notes:
|
|
3297
|
-
Before doing anything against Snyk you need to configure a token
|
|
3298
|
-
used for authentication. The following command will ask for a token
|
|
3299
|
-
and provide a link to generate one:
|
|
3300
|
-
$ cals snyk set-token`),
|
|
3301
|
-
handler: () => {
|
|
3302
|
-
yargs(hideBin(process__default.argv)).showHelp();
|
|
3303
|
-
},
|
|
3304
|
-
};
|
|
3305
|
-
|
|
3306
1517
|
async function main() {
|
|
3307
|
-
if (!
|
|
1518
|
+
if (!satisfiesMinVersion(process__default.version, engines.node)) {
|
|
3308
1519
|
console.error(`Required node version ${engines.node} not satisfied with current version ${process__default.version}.`);
|
|
3309
1520
|
process__default.exit(1);
|
|
3310
1521
|
}
|
|
3311
1522
|
await yargs(hideBin(process__default.argv))
|
|
3312
|
-
.usage(`cals
|
|
1523
|
+
.usage(`cals v${version} (build: ${"2026-01-30T15:20:39.822Z"})
|
|
1524
|
+
|
|
1525
|
+
A CLI for managing GitHub repositories.
|
|
1526
|
+
|
|
1527
|
+
Before using, authenticate with: cals auth`)
|
|
3313
1528
|
.scriptName("cals")
|
|
3314
1529
|
.locale("en")
|
|
1530
|
+
.strict()
|
|
1531
|
+
.strictCommands()
|
|
1532
|
+
.strictOptions()
|
|
3315
1533
|
.help("help")
|
|
3316
|
-
.command(command$
|
|
3317
|
-
.command(command$f)
|
|
1534
|
+
.command(command$5)
|
|
3318
1535
|
.command(command$4)
|
|
3319
|
-
.command(command$
|
|
1536
|
+
.command(command$3)
|
|
1537
|
+
.command(command$2)
|
|
1538
|
+
.command(command$1)
|
|
3320
1539
|
.command(command)
|
|
3321
1540
|
.version(version)
|
|
3322
1541
|
.demandCommand()
|
|
3323
|
-
.option("
|
|
3324
|
-
describe: "
|
|
3325
|
-
type: "boolean",
|
|
3326
|
-
})
|
|
3327
|
-
.option("verbose", {
|
|
3328
|
-
describe: "Verbose output",
|
|
3329
|
-
type: "boolean",
|
|
3330
|
-
})
|
|
3331
|
-
.option("validate-cache", {
|
|
3332
|
-
describe: "Only read from cache if validated against server",
|
|
1542
|
+
.option("no-cache", {
|
|
1543
|
+
describe: "Bypass cache and fetch fresh data",
|
|
3333
1544
|
type: "boolean",
|
|
3334
1545
|
})
|
|
1546
|
+
.example("cals auth", "Set GitHub token")
|
|
1547
|
+
.example("cals repos", "List repositories")
|
|
1548
|
+
.example("cals groups", "List repository groups")
|
|
1549
|
+
.example("cals topics", "List customer topics")
|
|
1550
|
+
.example("cals clone --all | bash", "Clone all repos")
|
|
1551
|
+
.example("cals sync", "Pull latest changes")
|
|
3335
1552
|
.parse();
|
|
3336
1553
|
}
|
|
3337
1554
|
|