@capraconsulting/cals-cli 3.13.0 → 3.14.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 +4 -16
- package/lib/cals-cli.mjs +599 -2461
- package/lib/cals-cli.mjs.map +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 -10
- package/lib/index.es.js +11 -1391
- package/lib/index.es.js.map +1 -1
- package/lib/index.js +11 -1391
- package/lib/index.js.map +1 -1
- package/package.json +10 -23
- package/lib/cli/commands/definition/dump-setup.d.ts +0 -3
- 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/definition/validate.d.ts +0 -3
- package/lib/cli/commands/definition.d.ts +0 -3
- package/lib/cli/commands/delete-cache.d.ts +0 -3
- package/lib/cli/commands/getting-started.d.ts +0 -3
- 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/list-pull-requests-stats.d.ts +0 -3
- package/lib/cli/commands/github/list-webhooks.d.ts +0 -3
- package/lib/cli/commands/github/util.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/sonarcloud/index.d.ts +0 -3
- package/lib/sonarcloud/service.d.ts +0 -33
- package/lib/sonarcloud/token.d.ts +0 -8
- 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/cals-cli.mjs
CHANGED
|
@@ -1,2215 +1,448 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import process
|
|
3
|
-
import
|
|
2
|
+
import * as process from 'node:process';
|
|
3
|
+
import process__default from 'node:process';
|
|
4
4
|
import yargs from 'yargs';
|
|
5
5
|
import { hideBin } from 'yargs/helpers';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
-
import
|
|
8
|
-
import pMap from 'p-map';
|
|
9
|
-
import AJV from 'ajv';
|
|
7
|
+
import path from 'node:path';
|
|
10
8
|
import { Buffer } from 'node:buffer';
|
|
11
9
|
import { performance } from 'node:perf_hooks';
|
|
12
10
|
import { Octokit } from '@octokit/rest';
|
|
13
|
-
import fetch from 'node-fetch';
|
|
14
11
|
import pLimit from 'p-limit';
|
|
15
|
-
import * as process$1 from 'process';
|
|
16
12
|
import keytar from 'keytar';
|
|
17
|
-
import { deprecate } from 'node:util';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import https from 'node:https';
|
|
20
13
|
import os from 'node:os';
|
|
21
14
|
import cachedir from 'cachedir';
|
|
22
15
|
import readline from 'node:readline';
|
|
23
16
|
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.14.0";
|
|
30
23
|
var engines = {
|
|
31
24
|
node: ">=22.14.0"
|
|
32
25
|
};
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
class GitHubTokenCliProvider {
|
|
28
|
+
keyringService = "cals";
|
|
29
|
+
keyringAccount = "github-token";
|
|
30
|
+
async getToken() {
|
|
31
|
+
if (process__default.env.CALS_GITHUB_TOKEN) {
|
|
32
|
+
return process__default.env.CALS_GITHUB_TOKEN;
|
|
33
|
+
}
|
|
34
|
+
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
35
|
+
if (result == null) {
|
|
36
|
+
process__default.stderr.write("No token found. Register using `cals github set-token`\n");
|
|
37
|
+
return undefined;
|
|
39
38
|
}
|
|
40
|
-
result[key].push(item);
|
|
41
39
|
return result;
|
|
42
|
-
}
|
|
40
|
+
}
|
|
41
|
+
async markInvalid() {
|
|
42
|
+
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
43
|
+
}
|
|
44
|
+
async setToken(value) {
|
|
45
|
+
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
|
|
49
|
+
class GitHubService {
|
|
50
|
+
octokit;
|
|
51
|
+
cache;
|
|
52
|
+
tokenProvider;
|
|
53
|
+
semaphore;
|
|
54
|
+
constructor(props) {
|
|
55
|
+
this.octokit = props.octokit;
|
|
56
|
+
this.cache = props.cache;
|
|
57
|
+
this.tokenProvider = props.tokenProvider;
|
|
58
|
+
// Control concurrency to GitHub API at service level so we
|
|
59
|
+
// can maximize concurrency all other places.
|
|
60
|
+
this.semaphore = pLimit(6);
|
|
61
|
+
this.octokit.hook.wrap("request", async (request, options) => {
|
|
62
|
+
if (options.method !== "GET") {
|
|
63
|
+
return this.semaphore(() => request(options));
|
|
64
|
+
}
|
|
65
|
+
// Try to cache ETag for GET requests to save on rate limiting.
|
|
66
|
+
// Hits on ETag does not count towards rate limiting.
|
|
67
|
+
const rest = {
|
|
68
|
+
...options,
|
|
69
|
+
};
|
|
70
|
+
delete rest.method;
|
|
71
|
+
delete rest.baseUrl;
|
|
72
|
+
delete rest.headers;
|
|
73
|
+
delete rest.mediaType;
|
|
74
|
+
delete rest.request;
|
|
75
|
+
// Build a key that is used to identify this request.
|
|
76
|
+
const key = Buffer.from(JSON.stringify(rest)).toString("base64");
|
|
77
|
+
const cacheItem = this.cache.retrieveJson(key);
|
|
78
|
+
if (cacheItem !== undefined) {
|
|
79
|
+
// Copying doesn't work, seems we need to mutate this.
|
|
80
|
+
options.headers["If-None-Match"] = cacheItem.data.etag;
|
|
81
|
+
}
|
|
82
|
+
const getResponse = async (allowRetry = true) => {
|
|
83
|
+
try {
|
|
84
|
+
return await request(options);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
// Handle no change in ETag.
|
|
88
|
+
if (e.status === 304) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// GitHub seems to throw a lot of 502 errors.
|
|
92
|
+
// Let's give it a few seconds and retry one time.
|
|
93
|
+
if (e.status === 502 && allowRetry) {
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
95
|
+
return await getResponse(false);
|
|
96
|
+
}
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const response = await this.semaphore(async () => {
|
|
101
|
+
return getResponse();
|
|
102
|
+
});
|
|
103
|
+
if (response === undefined) {
|
|
104
|
+
// Undefined is returned for cached data.
|
|
105
|
+
if (cacheItem === undefined) {
|
|
106
|
+
throw new Error("Missing expected cache item");
|
|
107
|
+
}
|
|
108
|
+
// Use previous value.
|
|
109
|
+
return cacheItem.data.data;
|
|
110
|
+
}
|
|
111
|
+
// New value. Store Etag.
|
|
112
|
+
if (response.headers.etag) {
|
|
113
|
+
this.cache.storeJson(key, {
|
|
114
|
+
etag: response.headers.etag,
|
|
115
|
+
data: response,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return response;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async runGraphqlQuery(query) {
|
|
122
|
+
const token = await this.tokenProvider.getToken();
|
|
123
|
+
if (token === undefined) {
|
|
124
|
+
throw new Error("Missing token for GitHub");
|
|
125
|
+
}
|
|
126
|
+
const url = "https://api.github.com/graphql";
|
|
127
|
+
const headers = {
|
|
128
|
+
Authorization: `Bearer ${token}`,
|
|
129
|
+
};
|
|
130
|
+
let requestDuration = -1;
|
|
131
|
+
const response = await this.semaphore(() => {
|
|
132
|
+
const requestStart = performance.now();
|
|
133
|
+
const result = fetch(url, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers,
|
|
136
|
+
body: JSON.stringify({ query }),
|
|
137
|
+
});
|
|
138
|
+
requestDuration = performance.now() - requestStart;
|
|
139
|
+
return result;
|
|
140
|
+
});
|
|
141
|
+
if (response.status === 401) {
|
|
142
|
+
process.stderr.write("Unauthorized\n");
|
|
143
|
+
await this.tokenProvider.markInvalid();
|
|
144
|
+
}
|
|
145
|
+
// If you get 502 after 10s, it is a timeout.
|
|
146
|
+
if (response.status === 502) {
|
|
147
|
+
throw new Error(`Response from Github likely timed out (10s max) after elapsed ${requestDuration}ms with status ${response.status}: ${await response.text()}`);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`Response from GitHub not OK (${response.status}): ${await response.text()}`);
|
|
151
|
+
}
|
|
152
|
+
const json = (await response.json());
|
|
153
|
+
if (json.errors) {
|
|
154
|
+
throw new Error(`Error from GitHub GraphQL API: ${JSON.stringify(json.errors)}`);
|
|
155
|
+
}
|
|
156
|
+
if (json.data == null) {
|
|
157
|
+
throw new Error("No data received from GitHub GraphQL API (unknown reason)");
|
|
158
|
+
}
|
|
159
|
+
return json.data;
|
|
160
|
+
}
|
|
161
|
+
async getOrgRepoList({ org }) {
|
|
162
|
+
const getQuery = (after) => `{
|
|
163
|
+
organization(login: "${org}") {
|
|
164
|
+
repositories(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
165
|
+
totalCount
|
|
166
|
+
pageInfo {
|
|
167
|
+
hasNextPage
|
|
168
|
+
endCursor
|
|
169
|
+
}
|
|
170
|
+
nodes {
|
|
171
|
+
name
|
|
172
|
+
owner {
|
|
173
|
+
login
|
|
174
|
+
}
|
|
175
|
+
defaultBranchRef {
|
|
176
|
+
name
|
|
177
|
+
}
|
|
178
|
+
createdAt
|
|
179
|
+
updatedAt
|
|
180
|
+
isArchived
|
|
181
|
+
sshUrl
|
|
182
|
+
repositoryTopics(first: 100) {
|
|
183
|
+
edges {
|
|
184
|
+
node {
|
|
185
|
+
topic {
|
|
186
|
+
name
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}`;
|
|
195
|
+
return this.cache.json(`repos-${org}`, async () => {
|
|
196
|
+
const repos = [];
|
|
197
|
+
let after = null;
|
|
198
|
+
while (true) {
|
|
199
|
+
const query = getQuery(after);
|
|
200
|
+
const res = await this.runGraphqlQuery(query);
|
|
201
|
+
if (res.organization == null) {
|
|
202
|
+
throw new Error("Missing organization");
|
|
203
|
+
}
|
|
204
|
+
if (res.organization.repositories.nodes == null) {
|
|
205
|
+
throw new Error("Missing organization nodes");
|
|
206
|
+
}
|
|
207
|
+
repos.push(...res.organization.repositories.nodes);
|
|
208
|
+
if (!res.organization.repositories.pageInfo.hasNextPage) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
after = res.organization.repositories.pageInfo.endCursor;
|
|
212
|
+
}
|
|
213
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
46
216
|
}
|
|
47
|
-
function
|
|
48
|
-
return
|
|
217
|
+
async function createOctokit(tokenProvider) {
|
|
218
|
+
return new Octokit({
|
|
219
|
+
auth: await tokenProvider.getToken(),
|
|
220
|
+
});
|
|
49
221
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
222
|
+
async function createGitHubService(props) {
|
|
223
|
+
const tokenProvider = props.tokenProvider ?? new GitHubTokenCliProvider();
|
|
224
|
+
return new GitHubService({
|
|
225
|
+
octokit: await createOctokit(tokenProvider),
|
|
226
|
+
cache: props.cache,
|
|
227
|
+
tokenProvider,
|
|
228
|
+
});
|
|
52
229
|
}
|
|
53
230
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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$2.env.CALS_GITHUB_TOKEN) {
|
|
468
|
-
return process$2.env.CALS_GITHUB_TOKEN;
|
|
469
|
-
}
|
|
470
|
-
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
471
|
-
if (result == null) {
|
|
472
|
-
process$2.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
|
-
}
|
|
1033
|
-
|
|
1034
|
-
class SnykTokenCliProvider {
|
|
1035
|
-
keyringService = "cals";
|
|
1036
|
-
keyringAccount = "snyk-token";
|
|
1037
|
-
async getToken() {
|
|
1038
|
-
if (process$2.env.CALS_SNYK_TOKEN) {
|
|
1039
|
-
return process$2.env.CALS_SNYK_TOKEN;
|
|
1040
|
-
}
|
|
1041
|
-
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
1042
|
-
if (result == null) {
|
|
1043
|
-
process$2.stderr.write("No token found. Register using `cals snyk set-token`\n");
|
|
1044
|
-
return undefined;
|
|
1045
|
-
}
|
|
1046
|
-
return result;
|
|
1047
|
-
}
|
|
1048
|
-
async markInvalid() {
|
|
1049
|
-
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
1050
|
-
}
|
|
1051
|
-
async setToken(value) {
|
|
1052
|
-
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
class SnykService {
|
|
1057
|
-
config;
|
|
1058
|
-
tokenProvider;
|
|
1059
|
-
constructor(props) {
|
|
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$2.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
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function createSnykService(props) {
|
|
1141
|
-
return new SnykService({
|
|
1142
|
-
config: props.config,
|
|
1143
|
-
tokenProvider: props.tokenProvider ?? new SnykTokenCliProvider(),
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
function getGitHubRepo(snykProject) {
|
|
1148
|
-
if (snykProject.origin === "github") {
|
|
1149
|
-
const match = /^([^/]+)\/([^:]+)(:(.+))?$/.exec(snykProject.name);
|
|
1150
|
-
if (match === null) {
|
|
1151
|
-
throw Error(`Could not extract components from Snyk project name: ${snykProject.name} (id: ${snykProject.id})`);
|
|
1152
|
-
}
|
|
1153
|
-
return {
|
|
1154
|
-
owner: match[1],
|
|
1155
|
-
name: match[2],
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
if (snykProject.origin === "cli" && snykProject.remoteRepoUrl != null) {
|
|
1159
|
-
// The remoteRepoUrl can be overridden when using the CLI, so don't
|
|
1160
|
-
// fail if we cannot extract the value.
|
|
1161
|
-
const match = /github.com\/([^/]+)\/(.+)\.git$/.exec(snykProject.remoteRepoUrl);
|
|
1162
|
-
if (match === null) {
|
|
1163
|
-
return undefined;
|
|
1164
|
-
}
|
|
1165
|
-
return {
|
|
1166
|
-
owner: match[1],
|
|
1167
|
-
name: match[2],
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
return undefined;
|
|
1171
|
-
}
|
|
1172
|
-
function getGitHubRepoId(repo) {
|
|
1173
|
-
return repo ? `${repo.owner}/${repo.name}` : undefined;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
class CacheProvider {
|
|
1177
|
-
constructor(config) {
|
|
1178
|
-
this.config = config;
|
|
1179
|
-
}
|
|
1180
|
-
mustValidate = false;
|
|
1181
|
-
config;
|
|
1182
|
-
defaultCacheTime = 1800;
|
|
1183
|
-
/**
|
|
1184
|
-
* Retrieve cache if existent, ignoring the time.
|
|
1185
|
-
*
|
|
1186
|
-
* The caller is responsible for handling proper validation,
|
|
1187
|
-
*/
|
|
1188
|
-
retrieveJson(cachekey) {
|
|
1189
|
-
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
1190
|
-
if (!fs.existsSync(cachefile)) {
|
|
1191
|
-
return undefined;
|
|
1192
|
-
}
|
|
1193
|
-
const data = fs.readFileSync(cachefile, "utf-8");
|
|
1194
|
-
return {
|
|
1195
|
-
cacheTime: fs.statSync(cachefile).mtime.getTime(),
|
|
1196
|
-
data: (data === "undefined" ? undefined : JSON.parse(data)),
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Save data to cache.
|
|
1201
|
-
*/
|
|
1202
|
-
storeJson(cachekey, data) {
|
|
1203
|
-
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
1204
|
-
if (!fs.existsSync(this.config.cacheDir)) {
|
|
1205
|
-
fs.mkdirSync(this.config.cacheDir, { recursive: true });
|
|
1206
|
-
}
|
|
1207
|
-
fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
|
|
1208
|
-
}
|
|
1209
|
-
async json(cachekey, block, cachetime = this.defaultCacheTime) {
|
|
1210
|
-
const cacheItem = this.mustValidate
|
|
1211
|
-
? undefined
|
|
1212
|
-
: this.retrieveJson(cachekey);
|
|
1213
|
-
const expire = new Date(Date.now() - cachetime * 1000).getTime();
|
|
1214
|
-
if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
|
|
1215
|
-
return cacheItem.data;
|
|
1216
|
-
}
|
|
1217
|
-
const result = await block();
|
|
1218
|
-
this.storeJson(cachekey, result);
|
|
1219
|
-
return result;
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Delete all cached data.
|
|
1223
|
-
*/
|
|
1224
|
-
cleanup() {
|
|
1225
|
-
fs.rmSync(this.config.cacheDir, { recursive: true, force: true });
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
class Config {
|
|
1230
|
-
cwd = path.resolve(process$2.cwd());
|
|
1231
|
-
configFile = path.join(os.homedir(), ".cals-config.json");
|
|
1232
|
-
cacheDir = cachedir("cals-cli");
|
|
1233
|
-
agent = new https.Agent({
|
|
1234
|
-
keepAlive: true,
|
|
1235
|
-
});
|
|
1236
|
-
configCached = undefined;
|
|
1237
|
-
get config() {
|
|
1238
|
-
const existingConfig = this.configCached;
|
|
1239
|
-
if (existingConfig !== undefined) {
|
|
1240
|
-
return existingConfig;
|
|
1241
|
-
}
|
|
1242
|
-
const config = this.readConfig();
|
|
1243
|
-
this.configCached = config;
|
|
1244
|
-
return config;
|
|
1245
|
-
}
|
|
1246
|
-
readConfig() {
|
|
1247
|
-
if (!fs.existsSync(this.configFile)) {
|
|
1248
|
-
return {};
|
|
1249
|
-
}
|
|
1250
|
-
try {
|
|
1251
|
-
return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
|
|
1252
|
-
}
|
|
1253
|
-
catch (e) {
|
|
1254
|
-
console.error("Failed", e);
|
|
1255
|
-
throw new Error("Failed to read config");
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
getConfig(key) {
|
|
1259
|
-
return this.config[key];
|
|
1260
|
-
}
|
|
1261
|
-
requireConfig(key) {
|
|
1262
|
-
const result = this.config[key];
|
|
1263
|
-
if (result === undefined) {
|
|
1264
|
-
throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
|
|
1265
|
-
}
|
|
1266
|
-
return result;
|
|
1267
|
-
}
|
|
1268
|
-
updateConfig(key, value) {
|
|
1269
|
-
const updatedConfig = {
|
|
1270
|
-
...this.readConfig(),
|
|
1271
|
-
[key]: value, // undefined will remove
|
|
1272
|
-
};
|
|
1273
|
-
fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
|
|
1274
|
-
this.configCached = updatedConfig;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
const CLEAR_WHOLE_LINE = 0;
|
|
1279
|
-
function clearLine(stdout) {
|
|
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$2.stdout;
|
|
1289
|
-
stderr = process$2.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
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
function createCacheProvider(config, argv) {
|
|
1318
|
-
const cache = new CacheProvider(config);
|
|
1319
|
-
// --validate-cache
|
|
1320
|
-
if (argv.validateCache === true) {
|
|
1321
|
-
cache.mustValidate = true;
|
|
1322
|
-
}
|
|
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
|
-
return cache;
|
|
1330
|
-
}
|
|
1331
|
-
function createConfig() {
|
|
1332
|
-
return new Config();
|
|
1333
|
-
}
|
|
1334
|
-
const definitionFileOptionName = "definition-file";
|
|
1335
|
-
const definitionFileOptionValue = {
|
|
1336
|
-
describe: "Path to definition file, which should be the latest resources.yaml file from https://github.com/capralifecycle/resources-definition",
|
|
1337
|
-
demandOption: true,
|
|
1338
|
-
type: "string",
|
|
1339
|
-
};
|
|
1340
|
-
function getDefinitionFile(argv) {
|
|
1341
|
-
if (argv.definitionFile === undefined) {
|
|
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$2.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);
|
|
231
|
+
function getGroup(repo) {
|
|
232
|
+
const projectTopics = [];
|
|
233
|
+
let isInfra = false;
|
|
234
|
+
repo.repositoryTopics.edges.forEach((edge) => {
|
|
235
|
+
const name = edge.node.topic.name;
|
|
236
|
+
if (name.startsWith("customer-")) {
|
|
237
|
+
projectTopics.push(name.substring(9));
|
|
1852
238
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
});
|
|
239
|
+
if (name.startsWith("project-")) {
|
|
240
|
+
projectTopics.push(name.substring(8));
|
|
241
|
+
}
|
|
242
|
+
if (name === "infrastructure") {
|
|
243
|
+
isInfra = true;
|
|
1859
244
|
}
|
|
1860
245
|
});
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
246
|
+
if (projectTopics.length > 1) {
|
|
247
|
+
console.warn(`Repo ${repo.name} has multiple project groups: ${projectTopics.join(", ")}. Picking first`);
|
|
248
|
+
}
|
|
249
|
+
if (projectTopics.length > 0) {
|
|
250
|
+
return projectTopics[0];
|
|
251
|
+
}
|
|
252
|
+
if (isInfra) {
|
|
253
|
+
return "infrastructure";
|
|
1867
254
|
}
|
|
1868
|
-
return
|
|
255
|
+
return null;
|
|
1869
256
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
*/
|
|
1873
|
-
async function createChangeSetItemsForTeams(github, definition, org) {
|
|
1874
|
-
const changes = [];
|
|
1875
|
-
const teams = (definition.github.teams.find((it) => it.organization === org.login) || {
|
|
1876
|
-
teams: [],
|
|
1877
|
-
}).teams;
|
|
1878
|
-
const actualTeams = await github.getTeamList(org);
|
|
1879
|
-
const actualTeamNames = actualTeams.map((it) => it.name);
|
|
1880
|
-
const wantedTeamNames = teams.map((it) => it.name);
|
|
1881
|
-
actualTeams
|
|
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,
|
|
1897
|
-
});
|
|
1898
|
-
// Must add all members when creating new team.
|
|
1899
|
-
for (const member of team.members) {
|
|
1900
|
-
changes.push({
|
|
1901
|
-
type: "team-member-add",
|
|
1902
|
-
org: org.login,
|
|
1903
|
-
team: team.name,
|
|
1904
|
-
user: member,
|
|
1905
|
-
// TODO: Allow to specify maintainers?
|
|
1906
|
-
role: "member",
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
});
|
|
1910
|
-
const overlappingTeams = actualTeams.filter((it) => wantedTeamNames.includes(it.name));
|
|
1911
|
-
await pMap(overlappingTeams, async (actualTeam) => {
|
|
1912
|
-
const wantedTeam = teams.find((it) => it.name === actualTeam.name);
|
|
1913
|
-
const actualMembers = await github.getTeamMemberListIncludingInvited(org, actualTeam);
|
|
1914
|
-
actualMembers
|
|
1915
|
-
.filter((it) => !wantedTeam.members.includes(it.login))
|
|
1916
|
-
.forEach((it) => {
|
|
1917
|
-
changes.push({
|
|
1918
|
-
type: "team-member-remove",
|
|
1919
|
-
org: org.login,
|
|
1920
|
-
team: actualTeam.name,
|
|
1921
|
-
user: it.login,
|
|
1922
|
-
});
|
|
1923
|
-
});
|
|
1924
|
-
const actualMembersNames = actualMembers.map((it) => it.login);
|
|
1925
|
-
wantedTeam.members
|
|
1926
|
-
.filter((it) => !actualMembersNames.includes(it))
|
|
1927
|
-
.forEach((it) => {
|
|
1928
|
-
changes.push({
|
|
1929
|
-
type: "team-member-add",
|
|
1930
|
-
org: org.login,
|
|
1931
|
-
team: actualTeam.name,
|
|
1932
|
-
user: it,
|
|
1933
|
-
// TODO: Allow to specify maintainers?
|
|
1934
|
-
role: "member",
|
|
1935
|
-
});
|
|
1936
|
-
});
|
|
1937
|
-
// TODO: team-member-permission (member/maintainer)
|
|
1938
|
-
});
|
|
1939
|
-
return changes;
|
|
257
|
+
function ifnull(a, other) {
|
|
258
|
+
return a === null ? other : a;
|
|
1940
259
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
260
|
+
function getGroupedRepos(repos) {
|
|
261
|
+
return Object.values(repos.reduce((acc, repo) => {
|
|
262
|
+
const group = ifnull(getGroup(repo), "(unknown)");
|
|
263
|
+
const value = acc[group] || { name: group, items: [] };
|
|
264
|
+
return {
|
|
265
|
+
...acc,
|
|
266
|
+
[group]: {
|
|
267
|
+
...value,
|
|
268
|
+
items: [...value.items, repo],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}, {})).sort((a, b) => a.name.localeCompare(b.name));
|
|
272
|
+
}
|
|
273
|
+
function includesTopic(repo, topic) {
|
|
274
|
+
return repo.repositoryTopics.edges.some((it) => it.node.topic.name === topic);
|
|
1951
275
|
}
|
|
1952
276
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
const orgCache = {};
|
|
1957
|
-
const orgTeamListCache = {};
|
|
1958
|
-
async function getOrg(orgName) {
|
|
1959
|
-
if (!(orgName in orgCache)) {
|
|
1960
|
-
orgCache[orgName] = await github.getOrg(orgName);
|
|
1961
|
-
}
|
|
1962
|
-
return orgCache[orgName];
|
|
277
|
+
class CacheProvider {
|
|
278
|
+
constructor(config) {
|
|
279
|
+
this.config = config;
|
|
1963
280
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
281
|
+
mustValidate = false;
|
|
282
|
+
config;
|
|
283
|
+
defaultCacheTime = 1800;
|
|
284
|
+
/**
|
|
285
|
+
* Retrieve cache if existent, ignoring the time.
|
|
286
|
+
*
|
|
287
|
+
* The caller is responsible for handling proper validation,
|
|
288
|
+
*/
|
|
289
|
+
retrieveJson(cachekey) {
|
|
290
|
+
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
291
|
+
if (!fs.existsSync(cachefile)) {
|
|
292
|
+
return undefined;
|
|
1968
293
|
}
|
|
1969
|
-
|
|
294
|
+
const data = fs.readFileSync(cachefile, "utf-8");
|
|
295
|
+
return {
|
|
296
|
+
cacheTime: fs.statSync(cachefile).mtime.getTime(),
|
|
297
|
+
data: (data === "undefined" ? undefined : JSON.parse(data)),
|
|
298
|
+
};
|
|
1970
299
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Save data to cache.
|
|
302
|
+
*/
|
|
303
|
+
storeJson(cachekey, data) {
|
|
304
|
+
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
305
|
+
if (!fs.existsSync(this.config.cacheDir)) {
|
|
306
|
+
fs.mkdirSync(this.config.cacheDir, { recursive: true });
|
|
1976
307
|
}
|
|
1977
|
-
|
|
308
|
+
fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
|
|
1978
309
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
const
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
function isNotImplementedChangeSetItem(changeItem) {
|
|
1987
|
-
return notImplementedChangeSetItems.includes(changeItem.type);
|
|
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;
|
|
2063
|
-
}
|
|
2064
|
-
else if ("wiki" in attrib) {
|
|
2065
|
-
upd.has_wiki = attrib.wiki;
|
|
2066
|
-
}
|
|
2067
|
-
else if ("private" in attrib) {
|
|
2068
|
-
upd.private = attrib.private;
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
await github.octokit.repos.update(upd);
|
|
2072
|
-
return true;
|
|
310
|
+
async json(cachekey, block, cachetime = this.defaultCacheTime) {
|
|
311
|
+
const cacheItem = this.mustValidate
|
|
312
|
+
? undefined
|
|
313
|
+
: this.retrieveJson(cachekey);
|
|
314
|
+
const expire = new Date(Date.now() - cachetime * 1000).getTime();
|
|
315
|
+
if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
|
|
316
|
+
return cacheItem.data;
|
|
2073
317
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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;
|
|
318
|
+
const result = await block();
|
|
319
|
+
this.storeJson(cachekey, result);
|
|
320
|
+
return result;
|
|
2094
321
|
}
|
|
2095
|
-
|
|
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);
|
|
322
|
+
/**
|
|
323
|
+
* Delete all cached data.
|
|
324
|
+
*/
|
|
325
|
+
cleanup() {
|
|
326
|
+
fs.rmSync(this.config.cacheDir, { recursive: true, force: true });
|
|
2104
327
|
}
|
|
2105
328
|
}
|
|
2106
329
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
330
|
+
class Config {
|
|
331
|
+
cwd = path.resolve(process__default.cwd());
|
|
332
|
+
configFile = path.join(os.homedir(), ".cals-config.json");
|
|
333
|
+
cacheDir = cachedir("cals-cli");
|
|
334
|
+
configCached = undefined;
|
|
335
|
+
get config() {
|
|
336
|
+
const existingConfig = this.configCached;
|
|
337
|
+
if (existingConfig !== undefined) {
|
|
338
|
+
return existingConfig;
|
|
2115
339
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
teams: await github.getTeamList(org),
|
|
2124
|
-
};
|
|
340
|
+
const config = this.readConfig();
|
|
341
|
+
this.configCached = config;
|
|
342
|
+
return config;
|
|
343
|
+
}
|
|
344
|
+
readConfig() {
|
|
345
|
+
if (!fs.existsSync(this.configFile)) {
|
|
346
|
+
return {};
|
|
2125
347
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
}
|
|
2129
|
-
async function process(reporter, github, definition, getOrg, execute, limitToOrg) {
|
|
2130
|
-
let changes = [];
|
|
2131
|
-
for (const orgName of getGitHubOrgs(definition)) {
|
|
2132
|
-
if (limitToOrg !== undefined && limitToOrg !== orgName) {
|
|
2133
|
-
continue;
|
|
348
|
+
try {
|
|
349
|
+
return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
|
|
2134
350
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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)}`);
|
|
351
|
+
catch (e) {
|
|
352
|
+
console.error("Failed", e);
|
|
353
|
+
throw new Error("Failed to read config");
|
|
2156
354
|
}
|
|
2157
355
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
356
|
+
getConfig(key) {
|
|
357
|
+
return this.config[key];
|
|
2160
358
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
359
|
+
requireConfig(key) {
|
|
360
|
+
const result = this.config[key];
|
|
361
|
+
if (result === undefined) {
|
|
362
|
+
throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
|
|
2165
363
|
}
|
|
364
|
+
return result;
|
|
2166
365
|
}
|
|
2167
|
-
|
|
2168
|
-
const
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
await executeChangeSet(github, changes, reporter);
|
|
2175
|
-
}
|
|
2176
|
-
else {
|
|
2177
|
-
reporter.info("Skipping");
|
|
2178
|
-
}
|
|
366
|
+
updateConfig(key, value) {
|
|
367
|
+
const updatedConfig = {
|
|
368
|
+
...this.readConfig(),
|
|
369
|
+
[key]: value, // undefined will remove
|
|
370
|
+
};
|
|
371
|
+
fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
|
|
372
|
+
this.configCached = updatedConfig;
|
|
2179
373
|
}
|
|
2180
|
-
reporter.info(`Number of GitHub requests: ${github.requestCount}`);
|
|
2181
374
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
.
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
.
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
const
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
375
|
+
|
|
376
|
+
const CLEAR_WHOLE_LINE = 0;
|
|
377
|
+
function clearLine(stdout) {
|
|
378
|
+
readline.clearLine(stdout, CLEAR_WHOLE_LINE);
|
|
379
|
+
readline.cursorTo(stdout, 0);
|
|
380
|
+
}
|
|
381
|
+
async function readInput(options) {
|
|
382
|
+
const rl = readline.createInterface({
|
|
383
|
+
input: process__default.stdin,
|
|
384
|
+
output: process__default.stdout,
|
|
385
|
+
});
|
|
386
|
+
if (options.silent) {
|
|
387
|
+
rl._writeToOutput = () => { };
|
|
388
|
+
}
|
|
389
|
+
return new Promise((resolve, reject) => {
|
|
390
|
+
const timer = options.timeout
|
|
391
|
+
? setTimeout(() => {
|
|
392
|
+
rl.close();
|
|
393
|
+
reject(new Error("Input timed out"));
|
|
394
|
+
}, options.timeout)
|
|
395
|
+
: null;
|
|
396
|
+
rl.question(options.prompt, (answer) => {
|
|
397
|
+
if (timer)
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
rl.close();
|
|
400
|
+
if (options.silent) {
|
|
401
|
+
process__default.stdout.write("\n");
|
|
402
|
+
}
|
|
403
|
+
resolve(answer);
|
|
2206
404
|
});
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
class Reporter {
|
|
408
|
+
stdout = process__default.stdout;
|
|
409
|
+
stderr = process__default.stderr;
|
|
410
|
+
format = chalk;
|
|
411
|
+
error(msg) {
|
|
412
|
+
clearLine(this.stderr);
|
|
413
|
+
this.stderr.write(`${this.format.red("error")} ${msg}\n`);
|
|
414
|
+
}
|
|
415
|
+
log(msg) {
|
|
416
|
+
clearLine(this.stdout);
|
|
417
|
+
this.stdout.write(`${msg}\n`);
|
|
418
|
+
}
|
|
419
|
+
warn(msg) {
|
|
420
|
+
clearLine(this.stderr);
|
|
421
|
+
this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
|
|
422
|
+
}
|
|
423
|
+
info(msg) {
|
|
424
|
+
clearLine(this.stdout);
|
|
425
|
+
this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function createReporter() {
|
|
430
|
+
return new Reporter();
|
|
431
|
+
}
|
|
432
|
+
function createCacheProvider(config, argv) {
|
|
433
|
+
const cache = new CacheProvider(config);
|
|
434
|
+
if (argv.validateCache === true) {
|
|
435
|
+
cache.mustValidate = true;
|
|
436
|
+
}
|
|
437
|
+
return cache;
|
|
438
|
+
}
|
|
439
|
+
function createConfig() {
|
|
440
|
+
return new Config();
|
|
441
|
+
}
|
|
2209
442
|
|
|
2210
443
|
async function generateCloneCommands({ reporter, config, github, org, ...opt }) {
|
|
2211
444
|
if (!opt.listGroups && !opt.all && opt.group === undefined) {
|
|
2212
|
-
yargs(hideBin(
|
|
445
|
+
yargs(hideBin(process__default.argv)).showHelp();
|
|
2213
446
|
return;
|
|
2214
447
|
}
|
|
2215
448
|
const repos = await github.getOrgRepoList({ org });
|
|
@@ -2233,11 +466,11 @@ async function generateCloneCommands({ reporter, config, github, org, ...opt })
|
|
|
2233
466
|
.forEach((repo) => {
|
|
2234
467
|
// The output of this is used to pipe into e.g. bash.
|
|
2235
468
|
// We cannot use reporter.log as it adds additional characters.
|
|
2236
|
-
|
|
469
|
+
process__default.stdout.write(`[ ! -e "${repo.name}" ] && git clone ${repo.sshUrl}\n`);
|
|
2237
470
|
});
|
|
2238
471
|
});
|
|
2239
472
|
}
|
|
2240
|
-
const command$
|
|
473
|
+
const command$4 = {
|
|
2241
474
|
command: "generate-clone-commands",
|
|
2242
475
|
describe: "Generate shell commands to clone GitHub repos for an organization",
|
|
2243
476
|
builder: (yargs) => yargs
|
|
@@ -2280,75 +513,19 @@ const command$a = {
|
|
|
2280
513
|
handler: async (argv) => {
|
|
2281
514
|
const config = createConfig();
|
|
2282
515
|
return generateCloneCommands({
|
|
2283
|
-
reporter: createReporter(
|
|
2284
|
-
config,
|
|
2285
|
-
github: await createGitHubService({
|
|
2286
|
-
config,
|
|
2287
|
-
cache: createCacheProvider(config, argv),
|
|
2288
|
-
}),
|
|
2289
|
-
all: !!argv.all,
|
|
2290
|
-
listGroups: !!argv["list-groups"],
|
|
2291
|
-
includeArchived: !!argv["include-archived"],
|
|
2292
|
-
name: argv.name,
|
|
2293
|
-
topic: argv.topic,
|
|
2294
|
-
excludeExisting: !!argv["exclude-existing"],
|
|
2295
|
-
group: argv.group,
|
|
2296
|
-
org: argv.org,
|
|
2297
|
-
});
|
|
2298
|
-
},
|
|
2299
|
-
};
|
|
2300
|
-
|
|
2301
|
-
async function listPullRequestsStats({ reporter, github, }) {
|
|
2302
|
-
// This is only an initial attempt to get some insights into
|
|
2303
|
-
// open pull requests. Feel free to change.
|
|
2304
|
-
const pulls = await github.getSearchedPullRequestList("capralifecycle");
|
|
2305
|
-
const cutoffOld = new Date(Date.now() - 86400 * 1000 * 60);
|
|
2306
|
-
const categories = pulls
|
|
2307
|
-
.reduce((acc, cur) => {
|
|
2308
|
-
const key = `${cur.baseRepository.owner.login}/${cur.baseRepository.name}`;
|
|
2309
|
-
const old = new Date(cur.createdAt) < cutoffOld;
|
|
2310
|
-
const snyk = cur.title.includes("[Snyk]");
|
|
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",
|
|
2344
|
-
handler: async (argv) => {
|
|
2345
|
-
const config = createConfig();
|
|
2346
|
-
await listPullRequestsStats({
|
|
2347
|
-
reporter: createReporter(argv),
|
|
516
|
+
reporter: createReporter(),
|
|
517
|
+
config,
|
|
2348
518
|
github: await createGitHubService({
|
|
2349
|
-
config,
|
|
2350
519
|
cache: createCacheProvider(config, argv),
|
|
2351
520
|
}),
|
|
521
|
+
all: !!argv.all,
|
|
522
|
+
listGroups: !!argv["list-groups"],
|
|
523
|
+
includeArchived: !!argv["include-archived"],
|
|
524
|
+
name: argv.name,
|
|
525
|
+
topic: argv.topic,
|
|
526
|
+
excludeExisting: !!argv["exclude-existing"],
|
|
527
|
+
group: argv.group,
|
|
528
|
+
org: argv.org,
|
|
2352
529
|
});
|
|
2353
530
|
},
|
|
2354
531
|
};
|
|
@@ -2377,7 +554,7 @@ async function listRepos({ reporter, github, includeArchived, name = undefined,
|
|
|
2377
554
|
}
|
|
2378
555
|
// All CSV output is done using direct stdout to avoid extra chars.
|
|
2379
556
|
if (csv) {
|
|
2380
|
-
|
|
557
|
+
process__default.stdout.write("reponame,group\n");
|
|
2381
558
|
}
|
|
2382
559
|
getGroupedRepos(repos).forEach((group) => {
|
|
2383
560
|
if (!csv && compact) {
|
|
@@ -2390,7 +567,7 @@ async function listRepos({ reporter, github, includeArchived, name = undefined,
|
|
|
2390
567
|
group.items.forEach((repo) => {
|
|
2391
568
|
if (csv) {
|
|
2392
569
|
// We assume we have no repos or group names with a comma in its name.
|
|
2393
|
-
|
|
570
|
+
process__default.stdout.write(`${repo.name},${group.name}\n`);
|
|
2394
571
|
return;
|
|
2395
572
|
}
|
|
2396
573
|
if (compact) {
|
|
@@ -2435,7 +612,7 @@ async function listRepos({ reporter, github, includeArchived, name = undefined,
|
|
|
2435
612
|
});
|
|
2436
613
|
}
|
|
2437
614
|
}
|
|
2438
|
-
const command$
|
|
615
|
+
const command$3 = {
|
|
2439
616
|
command: "list-repos",
|
|
2440
617
|
describe: "List Git repos for a GitHub organization",
|
|
2441
618
|
builder: (yargs) => yargs
|
|
@@ -2470,9 +647,8 @@ const command$8 = {
|
|
|
2470
647
|
handler: async (argv) => {
|
|
2471
648
|
const config = createConfig();
|
|
2472
649
|
await listRepos({
|
|
2473
|
-
reporter: createReporter(
|
|
650
|
+
reporter: createReporter(),
|
|
2474
651
|
github: await createGitHubService({
|
|
2475
|
-
config,
|
|
2476
652
|
cache: createCacheProvider(config, argv),
|
|
2477
653
|
}),
|
|
2478
654
|
includeArchived: !!argv["include-archived"],
|
|
@@ -2485,57 +661,11 @@ const command$8 = {
|
|
|
2485
661
|
},
|
|
2486
662
|
};
|
|
2487
663
|
|
|
2488
|
-
|
|
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
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
const command$7 = {
|
|
2520
|
-
command: "list-webhooks",
|
|
2521
|
-
describe: "List webhooks for repositories in for a GitHub organization",
|
|
2522
|
-
builder: (yargs) => yargs.options("org", {
|
|
2523
|
-
required: true,
|
|
2524
|
-
describe: "Specify GitHub organization",
|
|
2525
|
-
type: "string",
|
|
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, }) {
|
|
664
|
+
async function setToken({ reporter, token, tokenProvider, }) {
|
|
2535
665
|
if (token === undefined) {
|
|
2536
666
|
reporter.info("Need API token to talk to GitHub");
|
|
2537
667
|
reporter.info("https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook");
|
|
2538
|
-
const inputToken = await
|
|
668
|
+
const inputToken = await readInput({
|
|
2539
669
|
prompt: "Enter new GitHub API token: ",
|
|
2540
670
|
silent: true,
|
|
2541
671
|
});
|
|
@@ -2544,21 +674,179 @@ async function setToken$1({ reporter, token, tokenProvider, }) {
|
|
|
2544
674
|
await tokenProvider.setToken(token);
|
|
2545
675
|
reporter.info("Token saved");
|
|
2546
676
|
}
|
|
2547
|
-
const command$
|
|
677
|
+
const command$2 = {
|
|
2548
678
|
command: "set-token",
|
|
2549
679
|
describe: "Set GitHub token for API calls",
|
|
2550
680
|
builder: (yargs) => yargs.positional("token", {
|
|
2551
681
|
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
682
|
}),
|
|
2553
683
|
handler: async (argv) => {
|
|
2554
|
-
await setToken
|
|
2555
|
-
reporter: createReporter(
|
|
684
|
+
await setToken({
|
|
685
|
+
reporter: createReporter(),
|
|
2556
686
|
token: argv.token,
|
|
2557
687
|
tokenProvider: new GitHubTokenCliProvider(),
|
|
2558
688
|
});
|
|
2559
689
|
},
|
|
2560
690
|
};
|
|
2561
691
|
|
|
692
|
+
var type = "object";
|
|
693
|
+
var properties = {
|
|
694
|
+
projects: {
|
|
695
|
+
type: "array",
|
|
696
|
+
items: {
|
|
697
|
+
$ref: "#/definitions/Project"
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var required = [
|
|
702
|
+
"projects"
|
|
703
|
+
];
|
|
704
|
+
var definitions = {
|
|
705
|
+
Project: {
|
|
706
|
+
type: "object",
|
|
707
|
+
properties: {
|
|
708
|
+
name: {
|
|
709
|
+
type: "string"
|
|
710
|
+
},
|
|
711
|
+
github: {
|
|
712
|
+
type: "array",
|
|
713
|
+
items: {
|
|
714
|
+
type: "object",
|
|
715
|
+
properties: {
|
|
716
|
+
organization: {
|
|
717
|
+
type: "string"
|
|
718
|
+
},
|
|
719
|
+
repos: {
|
|
720
|
+
type: "array",
|
|
721
|
+
items: {
|
|
722
|
+
$ref: "#/definitions/DefinitionRepo"
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
required: [
|
|
727
|
+
"organization"
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
tags: {
|
|
732
|
+
type: "array",
|
|
733
|
+
items: {
|
|
734
|
+
type: "string"
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
required: [
|
|
739
|
+
"github",
|
|
740
|
+
"name"
|
|
741
|
+
]
|
|
742
|
+
},
|
|
743
|
+
DefinitionRepo: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
name: {
|
|
747
|
+
type: "string"
|
|
748
|
+
},
|
|
749
|
+
previousNames: {
|
|
750
|
+
type: "array",
|
|
751
|
+
items: {
|
|
752
|
+
$ref: "#/definitions/DefinitionRepoPreviousName"
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
archived: {
|
|
756
|
+
type: "boolean"
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
required: [
|
|
760
|
+
"name"
|
|
761
|
+
]
|
|
762
|
+
},
|
|
763
|
+
DefinitionRepoPreviousName: {
|
|
764
|
+
type: "object",
|
|
765
|
+
properties: {
|
|
766
|
+
name: {
|
|
767
|
+
type: "string"
|
|
768
|
+
},
|
|
769
|
+
project: {
|
|
770
|
+
type: "string"
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
required: [
|
|
774
|
+
"name",
|
|
775
|
+
"project"
|
|
776
|
+
]
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
var $schema = "http://json-schema.org/draft-07/schema#";
|
|
780
|
+
var schema = {
|
|
781
|
+
type: type,
|
|
782
|
+
properties: properties,
|
|
783
|
+
required: required,
|
|
784
|
+
definitions: definitions,
|
|
785
|
+
$schema: $schema
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
function getRepoId(orgName, repoName) {
|
|
789
|
+
return `${orgName}/${repoName}`;
|
|
790
|
+
}
|
|
791
|
+
function checkAgainstSchema(value) {
|
|
792
|
+
const ajv = new AJV({ allErrors: true });
|
|
793
|
+
const valid = ajv.validate(schema, value);
|
|
794
|
+
return valid
|
|
795
|
+
? { definition: value }
|
|
796
|
+
: { error: ajv.errorsText() ?? "Unknown error" };
|
|
797
|
+
}
|
|
798
|
+
function requireValidDefinition(definition) {
|
|
799
|
+
// Verify no duplicates in project names.
|
|
800
|
+
definition.projects.reduce((acc, project) => {
|
|
801
|
+
if (acc.includes(project.name)) {
|
|
802
|
+
throw new Error(`Duplicate project: ${project.name}`);
|
|
803
|
+
}
|
|
804
|
+
return [...acc, project.name];
|
|
805
|
+
}, []);
|
|
806
|
+
// Verify no duplicates in repos.
|
|
807
|
+
definition.projects
|
|
808
|
+
.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => getRepoId(org.organization, repo.name))))
|
|
809
|
+
.reduce((acc, repoName) => {
|
|
810
|
+
if (acc.includes(repoName)) {
|
|
811
|
+
throw new Error(`Duplicate repo: ${repoName}`);
|
|
812
|
+
}
|
|
813
|
+
return [...acc, repoName];
|
|
814
|
+
}, []);
|
|
815
|
+
}
|
|
816
|
+
class DefinitionFile {
|
|
817
|
+
path;
|
|
818
|
+
constructor(path) {
|
|
819
|
+
this.path = path;
|
|
820
|
+
}
|
|
821
|
+
async getContents() {
|
|
822
|
+
return new Promise((resolve, reject) => fs.readFile(this.path, "utf-8", (err, data) => {
|
|
823
|
+
if (err)
|
|
824
|
+
reject(err);
|
|
825
|
+
else
|
|
826
|
+
resolve(data);
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
async getDefinition() {
|
|
830
|
+
return parseDefinition(await this.getContents());
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function parseDefinition(value) {
|
|
834
|
+
const result = checkAgainstSchema(yaml.load(value));
|
|
835
|
+
if ("error" in result) {
|
|
836
|
+
throw new Error(`Definition content invalid: ${result.error}`);
|
|
837
|
+
}
|
|
838
|
+
requireValidDefinition(result.definition);
|
|
839
|
+
return result.definition;
|
|
840
|
+
}
|
|
841
|
+
function getRepos(definition) {
|
|
842
|
+
return definition.projects.flatMap((project) => project.github.flatMap((org) => (org.repos || []).map((repo) => ({
|
|
843
|
+
id: getRepoId(org.organization, repo.name),
|
|
844
|
+
orgName: org.organization,
|
|
845
|
+
project,
|
|
846
|
+
repo,
|
|
847
|
+
}))));
|
|
848
|
+
}
|
|
849
|
+
|
|
2562
850
|
function wasUpdated(output) {
|
|
2563
851
|
return output.startsWith("Updating ");
|
|
2564
852
|
}
|
|
@@ -2887,7 +1175,7 @@ async function getExpectedRepos(reporter, github, cals, rootdir) {
|
|
|
2887
1175
|
};
|
|
2888
1176
|
}
|
|
2889
1177
|
async function getInput(prompt) {
|
|
2890
|
-
return
|
|
1178
|
+
return readInput({
|
|
2891
1179
|
prompt,
|
|
2892
1180
|
timeout: 60000,
|
|
2893
1181
|
});
|
|
@@ -2912,7 +1200,7 @@ async function askMoveConfirm() {
|
|
|
2912
1200
|
return false;
|
|
2913
1201
|
}
|
|
2914
1202
|
}
|
|
2915
|
-
async function sync
|
|
1203
|
+
async function sync({ reporter, github, cals, rootdir, askClone, askMove, }) {
|
|
2916
1204
|
const { expectedRepos, definitionRepo } = await getExpectedRepos(reporter, github, cals, rootdir);
|
|
2917
1205
|
const unknownDirs = [];
|
|
2918
1206
|
const foundRepos = [];
|
|
@@ -2954,7 +1242,7 @@ async function sync$1({ reporter, github, cals, rootdir, askClone, askMove, }) {
|
|
|
2954
1242
|
for (const it of archivedRepos) {
|
|
2955
1243
|
reporter.info(` ${it.actualRelpath}`);
|
|
2956
1244
|
}
|
|
2957
|
-
const thisDirName = path.basename(
|
|
1245
|
+
const thisDirName = path.basename(process__default.cwd());
|
|
2958
1246
|
const archiveDir = `../${thisDirName}-archive`;
|
|
2959
1247
|
const hasArchiveDir = fs.existsSync(archiveDir);
|
|
2960
1248
|
if (hasArchiveDir) {
|
|
@@ -3036,7 +1324,7 @@ async function loadCalsManifest(config, reporter) {
|
|
|
3036
1324
|
const p = await findUp(CALS_YAML, { cwd: config.cwd });
|
|
3037
1325
|
if (p === undefined) {
|
|
3038
1326
|
reporter.error(`File ${CALS_YAML} not found. See help`);
|
|
3039
|
-
|
|
1327
|
+
process__default.exitCode = 1;
|
|
3040
1328
|
return null;
|
|
3041
1329
|
}
|
|
3042
1330
|
// TODO: Verify file has expected contents.
|
|
@@ -3050,7 +1338,7 @@ async function loadCalsManifest(config, reporter) {
|
|
|
3050
1338
|
cals,
|
|
3051
1339
|
};
|
|
3052
1340
|
}
|
|
3053
|
-
const command$
|
|
1341
|
+
const command$1 = {
|
|
3054
1342
|
command: "sync",
|
|
3055
1343
|
describe: "Sync repositories for working directory",
|
|
3056
1344
|
builder: (yargs) => yargs
|
|
@@ -3100,15 +1388,14 @@ will be stored there.`),
|
|
|
3100
1388
|
handler: async (argv) => {
|
|
3101
1389
|
const config = createConfig();
|
|
3102
1390
|
const github = await createGitHubService({
|
|
3103
|
-
config,
|
|
3104
1391
|
cache: createCacheProvider(config, argv),
|
|
3105
1392
|
});
|
|
3106
|
-
const reporter = createReporter(
|
|
1393
|
+
const reporter = createReporter();
|
|
3107
1394
|
const manifest = await loadCalsManifest(config, reporter);
|
|
3108
1395
|
if (manifest === null)
|
|
3109
1396
|
return;
|
|
3110
1397
|
const { dir, cals } = manifest;
|
|
3111
|
-
return sync
|
|
1398
|
+
return sync({
|
|
3112
1399
|
reporter,
|
|
3113
1400
|
github,
|
|
3114
1401
|
cals,
|
|
@@ -3119,18 +1406,14 @@ will be stored there.`),
|
|
|
3119
1406
|
},
|
|
3120
1407
|
};
|
|
3121
1408
|
|
|
3122
|
-
const command
|
|
1409
|
+
const command = {
|
|
3123
1410
|
command: "github",
|
|
3124
1411
|
describe: "Integration with GitHub",
|
|
3125
1412
|
builder: (yargs) => yargs
|
|
3126
|
-
.command(command$
|
|
3127
|
-
.command(command$
|
|
3128
|
-
.command(command$
|
|
3129
|
-
.command(command$
|
|
3130
|
-
.command(command$8)
|
|
3131
|
-
.command(command$7)
|
|
3132
|
-
.command(command$6)
|
|
3133
|
-
.command(command$5)
|
|
1413
|
+
.command(command$4)
|
|
1414
|
+
.command(command$3)
|
|
1415
|
+
.command(command$2)
|
|
1416
|
+
.command(command$1)
|
|
3134
1417
|
.demandCommand()
|
|
3135
1418
|
.usage(`cals github
|
|
3136
1419
|
|
|
@@ -3146,188 +1429,43 @@ Notes:
|
|
|
3146
1429
|
And for a specific project:
|
|
3147
1430
|
$ cals github generate-clone-commands --org capralifecycle -x buildtools | bash
|
|
3148
1431
|
|
|
3149
|
-
Keeping up to date with removed/renamed repos:
|
|
3150
|
-
$ cals github analyze-directory --org capralifecycle
|
|
3151
|
-
|
|
3152
1432
|
Some responses are cached for some time. Use the --validate-cache
|
|
3153
|
-
option to avoid stale cache
|
|
3154
|
-
the "cals delete-cache" command.`),
|
|
1433
|
+
option to avoid stale cache.`),
|
|
3155
1434
|
handler: () => {
|
|
3156
|
-
yargs(hideBin(
|
|
1435
|
+
yargs(hideBin(process__default.argv)).showHelp();
|
|
3157
1436
|
},
|
|
3158
1437
|
};
|
|
3159
1438
|
|
|
3160
|
-
function
|
|
3161
|
-
return
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
}
|
|
3166
|
-
function
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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),
|
|
3224
|
-
}),
|
|
3225
|
-
};
|
|
3226
|
-
|
|
3227
|
-
async function setToken({ reporter, token, tokenProvider, }) {
|
|
3228
|
-
if (token === undefined) {
|
|
3229
|
-
reporter.info("Need API token to talk to Snyk");
|
|
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,
|
|
3235
|
-
});
|
|
3236
|
-
token = inputToken;
|
|
3237
|
-
}
|
|
3238
|
-
await tokenProvider.setToken(token);
|
|
3239
|
-
reporter.info("Token saved");
|
|
3240
|
-
}
|
|
3241
|
-
const command$2 = {
|
|
3242
|
-
command: "set-token",
|
|
3243
|
-
describe: "Set Snyk token for API calls",
|
|
3244
|
-
builder: (yargs) => yargs.positional("token", {
|
|
3245
|
-
describe: "Token. If not provided it will be requested as input",
|
|
3246
|
-
}),
|
|
3247
|
-
handler: async (argv) => setToken({
|
|
3248
|
-
reporter: createReporter(argv),
|
|
3249
|
-
token: argv.token,
|
|
3250
|
-
tokenProvider: new SnykTokenCliProvider(),
|
|
3251
|
-
}),
|
|
3252
|
-
};
|
|
3253
|
-
|
|
3254
|
-
async function sync({ reporter, snyk, definitionFile, }) {
|
|
3255
|
-
const definition = await definitionFile.getDefinition();
|
|
3256
|
-
const knownRepos = (await snyk.getProjects(definition))
|
|
3257
|
-
.map((it) => getGitHubRepo(it))
|
|
3258
|
-
.filter((it) => it !== undefined);
|
|
3259
|
-
const allReposWithSnyk = getRepos(definition).filter((it) => it.repo.snyk === true);
|
|
3260
|
-
const allReposWithSnykStr = allReposWithSnyk.map((it) => getRepoId(it.orgName, it.repo.name));
|
|
3261
|
-
const missingInSnyk = allReposWithSnyk.filter((it) => !knownRepos.some((r) => r.owner === it.orgName && r.name === it.repo.name));
|
|
3262
|
-
const extraInSnyk = knownRepos.filter((it) => !allReposWithSnykStr.includes(`${it.owner}/${it.name}`));
|
|
3263
|
-
if (missingInSnyk.length === 0) {
|
|
3264
|
-
reporter.info("All seems fine");
|
|
3265
|
-
}
|
|
3266
|
-
else {
|
|
3267
|
-
missingInSnyk.forEach((it) => {
|
|
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
|
-
});
|
|
1439
|
+
function parseVersion(v) {
|
|
1440
|
+
return v
|
|
1441
|
+
.replace(/^[^\d]*/, "")
|
|
1442
|
+
.split(".")
|
|
1443
|
+
.map(Number);
|
|
1444
|
+
}
|
|
1445
|
+
function satisfiesMinVersion(current, required) {
|
|
1446
|
+
const cur = parseVersion(current);
|
|
1447
|
+
const req = parseVersion(required.replace(/^>=?\s*/, ""));
|
|
1448
|
+
for (let i = 0; i < 3; i++) {
|
|
1449
|
+
if ((cur[i] ?? 0) > (req[i] ?? 0))
|
|
1450
|
+
return true;
|
|
1451
|
+
if ((cur[i] ?? 0) < (req[i] ?? 0))
|
|
1452
|
+
return false;
|
|
3273
1453
|
}
|
|
1454
|
+
return true;
|
|
3274
1455
|
}
|
|
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$2.argv)).showHelp();
|
|
3303
|
-
},
|
|
3304
|
-
};
|
|
3305
|
-
|
|
3306
1456
|
async function main() {
|
|
3307
|
-
if (!
|
|
3308
|
-
console.error(`Required node version ${engines.node} not satisfied with current version ${
|
|
3309
|
-
|
|
1457
|
+
if (!satisfiesMinVersion(process__default.version, engines.node)) {
|
|
1458
|
+
console.error(`Required node version ${engines.node} not satisfied with current version ${process__default.version}.`);
|
|
1459
|
+
process__default.exit(1);
|
|
3310
1460
|
}
|
|
3311
|
-
await yargs(hideBin(
|
|
3312
|
-
.usage(`cals-cli v${version} (build: ${"2026-01-
|
|
1461
|
+
await yargs(hideBin(process__default.argv))
|
|
1462
|
+
.usage(`cals-cli v${version} (build: ${"2026-01-30T14:13:10.419Z"})`)
|
|
3313
1463
|
.scriptName("cals")
|
|
3314
1464
|
.locale("en")
|
|
3315
1465
|
.help("help")
|
|
3316
|
-
.command(command$e)
|
|
3317
|
-
.command(command$f)
|
|
3318
|
-
.command(command$4)
|
|
3319
|
-
.command(command$d)
|
|
3320
1466
|
.command(command)
|
|
3321
1467
|
.version(version)
|
|
3322
1468
|
.demandCommand()
|
|
3323
|
-
.option("non-interactive", {
|
|
3324
|
-
describe: "Non-interactive mode",
|
|
3325
|
-
type: "boolean",
|
|
3326
|
-
})
|
|
3327
|
-
.option("verbose", {
|
|
3328
|
-
describe: "Verbose output",
|
|
3329
|
-
type: "boolean",
|
|
3330
|
-
})
|
|
3331
1469
|
.option("validate-cache", {
|
|
3332
1470
|
describe: "Only read from cache if validated against server",
|
|
3333
1471
|
type: "boolean",
|