@capraconsulting/cals-cli 2.25.21 → 2.25.23
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/lib/cache.d.ts +27 -27
- package/lib/cals-cli.d.ts +1 -1
- package/lib/cals-cli.js +2748 -2748
- package/lib/cli/commands/definition/dump-setup.d.ts +3 -3
- package/lib/cli/commands/definition/util.d.ts +6 -6
- package/lib/cli/commands/definition/util.test.d.ts +1 -1
- package/lib/cli/commands/definition/validate.d.ts +3 -3
- package/lib/cli/commands/definition.d.ts +3 -3
- package/lib/cli/commands/delete-cache.d.ts +3 -3
- package/lib/cli/commands/getting-started.d.ts +3 -3
- package/lib/cli/commands/github/analyze-directory.d.ts +3 -3
- package/lib/cli/commands/github/configure.d.ts +3 -3
- package/lib/cli/commands/github/generate-clone-commands.d.ts +3 -3
- package/lib/cli/commands/github/list-pull-requests-stats.d.ts +3 -3
- package/lib/cli/commands/github/list-repos.d.ts +3 -3
- package/lib/cli/commands/github/list-webhooks.d.ts +3 -3
- package/lib/cli/commands/github/set-token.d.ts +3 -3
- package/lib/cli/commands/github/sync.d.ts +9 -9
- package/lib/cli/commands/github/util.d.ts +3 -3
- package/lib/cli/commands/github.d.ts +3 -3
- package/lib/cli/commands/snyk/report.d.ts +3 -3
- package/lib/cli/commands/snyk/set-token.d.ts +3 -3
- package/lib/cli/commands/snyk/sync.d.ts +3 -3
- package/lib/cli/commands/snyk.d.ts +3 -3
- package/lib/cli/index.d.ts +1 -1
- package/lib/cli/index.test.d.ts +1 -1
- package/lib/cli/reporter.d.ts +27 -27
- package/lib/cli/util.d.ts +11 -11
- package/lib/config.d.ts +14 -14
- package/lib/definition/definition.d.ts +13 -13
- package/lib/definition/definition.test.d.ts +1 -1
- package/lib/definition/index.d.ts +2 -2
- package/lib/definition/types.d.ts +78 -78
- package/lib/git/GitRepo.d.ts +31 -31
- package/lib/git/util.d.ts +16 -16
- package/lib/git/util.test.d.ts +1 -1
- package/lib/github/changeset/changeset.d.ts +21 -21
- package/lib/github/changeset/execute.d.ts +10 -10
- package/lib/github/changeset/types.d.ts +93 -93
- package/lib/github/index.d.ts +2 -2
- package/lib/github/service.d.ts +91 -91
- package/lib/github/token.d.ts +11 -11
- package/lib/github/types.d.ts +85 -85
- package/lib/github/util.d.ts +8 -8
- package/lib/index.d.ts +14 -14
- package/lib/index.es.js +1519 -1519
- package/lib/index.js +1519 -1519
- package/lib/load-secrets/index.d.ts +2 -2
- package/lib/load-secrets/load-secrets.d.ts +7 -7
- package/lib/load-secrets/types.d.ts +22 -22
- package/lib/snyk/index.d.ts +3 -3
- package/lib/snyk/service.d.ts +21 -21
- package/lib/snyk/token.d.ts +11 -11
- package/lib/snyk/types.d.ts +27 -27
- package/lib/snyk/util.d.ts +3 -3
- package/lib/snyk/util.test.d.ts +1 -1
- package/lib/sonarcloud/index.d.ts +2 -2
- package/lib/sonarcloud/service.d.ts +33 -33
- package/lib/sonarcloud/token.d.ts +8 -8
- package/lib/testing/executor.d.ts +25 -25
- package/lib/testing/index.d.ts +2 -2
- package/lib/testing/lib.d.ts +64 -64
- package/package.json +6 -6
package/lib/index.es.js
CHANGED
|
@@ -22,148 +22,148 @@ import execa from 'execa';
|
|
|
22
22
|
import { performance } from 'perf_hooks';
|
|
23
23
|
import { Transform } from 'stream';
|
|
24
24
|
|
|
25
|
-
var version = "2.25.
|
|
25
|
+
var version = "2.25.23";
|
|
26
26
|
|
|
27
|
-
class CacheProvider {
|
|
28
|
-
constructor(config) {
|
|
29
|
-
this.mustValidate = false;
|
|
30
|
-
this.defaultCacheTime = 1800;
|
|
31
|
-
this.config = config;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Retrieve cache if existent, ignoring the time.
|
|
35
|
-
*
|
|
36
|
-
* The caller is responsible for handling proper validation,
|
|
37
|
-
*/
|
|
38
|
-
retrieveJson(cachekey) {
|
|
39
|
-
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
40
|
-
if (!fs.existsSync(cachefile)) {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
const data = fs.readFileSync(cachefile, "utf-8");
|
|
44
|
-
return {
|
|
45
|
-
cacheTime: fs.statSync(cachefile).mtime.getTime(),
|
|
46
|
-
data: (data === "undefined" ? undefined : JSON.parse(data)),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Save data to cache.
|
|
51
|
-
*/
|
|
52
|
-
storeJson(cachekey, data) {
|
|
53
|
-
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
54
|
-
if (!fs.existsSync(this.config.cacheDir)) {
|
|
55
|
-
fs.mkdirSync(this.config.cacheDir, { recursive: true });
|
|
56
|
-
}
|
|
57
|
-
fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
|
|
58
|
-
}
|
|
59
|
-
async json(cachekey, block, cachetime = this.defaultCacheTime) {
|
|
60
|
-
const cacheItem = this.mustValidate
|
|
61
|
-
? undefined
|
|
62
|
-
: this.retrieveJson(cachekey);
|
|
63
|
-
const expire = new Date(new Date().getTime() - cachetime * 1000).getTime();
|
|
64
|
-
if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
|
|
65
|
-
return cacheItem.data;
|
|
66
|
-
}
|
|
67
|
-
const result = await block();
|
|
68
|
-
this.storeJson(cachekey, result);
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Delete all cached data.
|
|
73
|
-
*/
|
|
74
|
-
cleanup() {
|
|
75
|
-
rimraf.sync(this.config.cacheDir);
|
|
76
|
-
}
|
|
27
|
+
class CacheProvider {
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.mustValidate = false;
|
|
30
|
+
this.defaultCacheTime = 1800;
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve cache if existent, ignoring the time.
|
|
35
|
+
*
|
|
36
|
+
* The caller is responsible for handling proper validation,
|
|
37
|
+
*/
|
|
38
|
+
retrieveJson(cachekey) {
|
|
39
|
+
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
40
|
+
if (!fs.existsSync(cachefile)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const data = fs.readFileSync(cachefile, "utf-8");
|
|
44
|
+
return {
|
|
45
|
+
cacheTime: fs.statSync(cachefile).mtime.getTime(),
|
|
46
|
+
data: (data === "undefined" ? undefined : JSON.parse(data)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Save data to cache.
|
|
51
|
+
*/
|
|
52
|
+
storeJson(cachekey, data) {
|
|
53
|
+
const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
|
|
54
|
+
if (!fs.existsSync(this.config.cacheDir)) {
|
|
55
|
+
fs.mkdirSync(this.config.cacheDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
|
|
58
|
+
}
|
|
59
|
+
async json(cachekey, block, cachetime = this.defaultCacheTime) {
|
|
60
|
+
const cacheItem = this.mustValidate
|
|
61
|
+
? undefined
|
|
62
|
+
: this.retrieveJson(cachekey);
|
|
63
|
+
const expire = new Date(new Date().getTime() - cachetime * 1000).getTime();
|
|
64
|
+
if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
|
|
65
|
+
return cacheItem.data;
|
|
66
|
+
}
|
|
67
|
+
const result = await block();
|
|
68
|
+
this.storeJson(cachekey, result);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Delete all cached data.
|
|
73
|
+
*/
|
|
74
|
+
cleanup() {
|
|
75
|
+
rimraf.sync(this.config.cacheDir);
|
|
76
|
+
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const CLEAR_WHOLE_LINE = 0;
|
|
80
|
-
function clearLine(stdout) {
|
|
81
|
-
readline.clearLine(stdout, CLEAR_WHOLE_LINE);
|
|
82
|
-
readline.cursorTo(stdout, 0);
|
|
83
|
-
}
|
|
84
|
-
class Reporter {
|
|
85
|
-
constructor(opts = {}) {
|
|
86
|
-
this.stdout = process.stdout;
|
|
87
|
-
this.stderr = process.stderr;
|
|
88
|
-
this.stdin = process.stdin;
|
|
89
|
-
this.isTTY = this.stdout.isTTY;
|
|
90
|
-
this.format = chalk;
|
|
91
|
-
this.startTime = Date.now();
|
|
92
|
-
this.nonInteractive = !!opts.nonInteractive;
|
|
93
|
-
this.isVerbose = !!opts.verbose;
|
|
94
|
-
}
|
|
95
|
-
error(msg) {
|
|
96
|
-
clearLine(this.stderr);
|
|
97
|
-
this.stderr.write(`${this.format.red("error")} ${msg}\n`);
|
|
98
|
-
}
|
|
99
|
-
log(msg) {
|
|
100
|
-
clearLine(this.stdout);
|
|
101
|
-
this.stdout.write(`${msg}\n`);
|
|
102
|
-
}
|
|
103
|
-
warn(msg) {
|
|
104
|
-
clearLine(this.stderr);
|
|
105
|
-
this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
|
|
106
|
-
}
|
|
107
|
-
success(msg) {
|
|
108
|
-
clearLine(this.stdout);
|
|
109
|
-
this.stdout.write(`${this.format.green("success")} ${msg}\n`);
|
|
110
|
-
}
|
|
111
|
-
info(msg) {
|
|
112
|
-
clearLine(this.stdout);
|
|
113
|
-
this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
|
|
114
|
-
}
|
|
79
|
+
const CLEAR_WHOLE_LINE = 0;
|
|
80
|
+
function clearLine(stdout) {
|
|
81
|
+
readline.clearLine(stdout, CLEAR_WHOLE_LINE);
|
|
82
|
+
readline.cursorTo(stdout, 0);
|
|
83
|
+
}
|
|
84
|
+
class Reporter {
|
|
85
|
+
constructor(opts = {}) {
|
|
86
|
+
this.stdout = process.stdout;
|
|
87
|
+
this.stderr = process.stderr;
|
|
88
|
+
this.stdin = process.stdin;
|
|
89
|
+
this.isTTY = this.stdout.isTTY;
|
|
90
|
+
this.format = chalk;
|
|
91
|
+
this.startTime = Date.now();
|
|
92
|
+
this.nonInteractive = !!opts.nonInteractive;
|
|
93
|
+
this.isVerbose = !!opts.verbose;
|
|
94
|
+
}
|
|
95
|
+
error(msg) {
|
|
96
|
+
clearLine(this.stderr);
|
|
97
|
+
this.stderr.write(`${this.format.red("error")} ${msg}\n`);
|
|
98
|
+
}
|
|
99
|
+
log(msg) {
|
|
100
|
+
clearLine(this.stdout);
|
|
101
|
+
this.stdout.write(`${msg}\n`);
|
|
102
|
+
}
|
|
103
|
+
warn(msg) {
|
|
104
|
+
clearLine(this.stderr);
|
|
105
|
+
this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
|
|
106
|
+
}
|
|
107
|
+
success(msg) {
|
|
108
|
+
clearLine(this.stdout);
|
|
109
|
+
this.stdout.write(`${this.format.green("success")} ${msg}\n`);
|
|
110
|
+
}
|
|
111
|
+
info(msg) {
|
|
112
|
+
clearLine(this.stdout);
|
|
113
|
+
this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
|
|
114
|
+
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
class Config {
|
|
118
|
-
constructor() {
|
|
119
|
-
this.cwd = path.resolve(process.cwd());
|
|
120
|
-
this.configFile = path.join(os.homedir(), ".cals-config.json");
|
|
121
|
-
this.cacheDir = cachedir("cals-cli");
|
|
122
|
-
this.agent = new https.Agent({
|
|
123
|
-
keepAlive: true,
|
|
124
|
-
});
|
|
125
|
-
this.configCached = undefined;
|
|
126
|
-
}
|
|
127
|
-
get config() {
|
|
128
|
-
const existingConfig = this.configCached;
|
|
129
|
-
if (existingConfig !== undefined) {
|
|
130
|
-
return existingConfig;
|
|
131
|
-
}
|
|
132
|
-
const config = this.readConfig();
|
|
133
|
-
this.configCached = config;
|
|
134
|
-
return config;
|
|
135
|
-
}
|
|
136
|
-
readConfig() {
|
|
137
|
-
if (!fs.existsSync(this.configFile)) {
|
|
138
|
-
return {};
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
142
|
-
return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
console.error("Failed", e);
|
|
146
|
-
throw new Error("Failed to read config");
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
getConfig(key) {
|
|
150
|
-
return this.config[key];
|
|
151
|
-
}
|
|
152
|
-
requireConfig(key) {
|
|
153
|
-
const result = this.config[key];
|
|
154
|
-
if (result === undefined) {
|
|
155
|
-
throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
|
|
156
|
-
}
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
updateConfig(key, value) {
|
|
160
|
-
const updatedConfig = {
|
|
161
|
-
...this.readConfig(),
|
|
162
|
-
[key]: value, // undefined will remove
|
|
163
|
-
};
|
|
164
|
-
fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
|
|
165
|
-
this.configCached = updatedConfig;
|
|
166
|
-
}
|
|
117
|
+
class Config {
|
|
118
|
+
constructor() {
|
|
119
|
+
this.cwd = path.resolve(process.cwd());
|
|
120
|
+
this.configFile = path.join(os.homedir(), ".cals-config.json");
|
|
121
|
+
this.cacheDir = cachedir("cals-cli");
|
|
122
|
+
this.agent = new https.Agent({
|
|
123
|
+
keepAlive: true,
|
|
124
|
+
});
|
|
125
|
+
this.configCached = undefined;
|
|
126
|
+
}
|
|
127
|
+
get config() {
|
|
128
|
+
const existingConfig = this.configCached;
|
|
129
|
+
if (existingConfig !== undefined) {
|
|
130
|
+
return existingConfig;
|
|
131
|
+
}
|
|
132
|
+
const config = this.readConfig();
|
|
133
|
+
this.configCached = config;
|
|
134
|
+
return config;
|
|
135
|
+
}
|
|
136
|
+
readConfig() {
|
|
137
|
+
if (!fs.existsSync(this.configFile)) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
142
|
+
return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.error("Failed", e);
|
|
146
|
+
throw new Error("Failed to read config");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
getConfig(key) {
|
|
150
|
+
return this.config[key];
|
|
151
|
+
}
|
|
152
|
+
requireConfig(key) {
|
|
153
|
+
const result = this.config[key];
|
|
154
|
+
if (result === undefined) {
|
|
155
|
+
throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
updateConfig(key, value) {
|
|
160
|
+
const updatedConfig = {
|
|
161
|
+
...this.readConfig(),
|
|
162
|
+
[key]: value, // undefined will remove
|
|
163
|
+
};
|
|
164
|
+
fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
|
|
165
|
+
this.configCached = updatedConfig;
|
|
166
|
+
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
var type = "object";
|
|
@@ -466,282 +466,282 @@ var schema = {
|
|
|
466
466
|
$schema: $schema
|
|
467
467
|
};
|
|
468
468
|
|
|
469
|
-
function getTeamId(org, teamName) {
|
|
470
|
-
return `${org}/${teamName}`;
|
|
471
|
-
}
|
|
472
|
-
function getRepoId(orgName, repoName) {
|
|
473
|
-
return `${orgName}/${repoName}`;
|
|
474
|
-
}
|
|
475
|
-
function checkAgainstSchema(value) {
|
|
476
|
-
var _a;
|
|
477
|
-
const ajv = new AJV({ allErrors: true });
|
|
478
|
-
const valid = ajv.validate(schema, value);
|
|
479
|
-
return valid
|
|
480
|
-
? { definition: value }
|
|
481
|
-
: { error: (_a = ajv.errorsText()) !== null && _a !== void 0 ? _a : "Unknown error" };
|
|
482
|
-
}
|
|
483
|
-
function requireValidDefinition(definition) {
|
|
484
|
-
// Verify no duplicates in users and extract known logins.
|
|
485
|
-
const loginList = definition.github.users.reduce((acc, user) => {
|
|
486
|
-
if (acc.includes(user.login)) {
|
|
487
|
-
throw new Error(`Duplicate login: ${user.login}`);
|
|
488
|
-
}
|
|
489
|
-
return [...acc, user.login];
|
|
490
|
-
}, []);
|
|
491
|
-
// Verify no duplicates in teams and extract team names.
|
|
492
|
-
const teamIdList = definition.github.teams.reduce((acc, orgTeams) => {
|
|
493
|
-
return orgTeams.teams.reduce((acc1, team) => {
|
|
494
|
-
const id = getTeamId(orgTeams.organization, team.name);
|
|
495
|
-
if (acc1.includes(id)) {
|
|
496
|
-
throw new Error(`Duplicate team: ${id}`);
|
|
497
|
-
}
|
|
498
|
-
return [...acc1, id];
|
|
499
|
-
}, acc);
|
|
500
|
-
}, []);
|
|
501
|
-
// Verify team members exists as users.
|
|
502
|
-
definition.github.teams
|
|
503
|
-
.map((it) => it.teams)
|
|
504
|
-
.flat()
|
|
505
|
-
.forEach((team) => {
|
|
506
|
-
team.members.forEach((login) => {
|
|
507
|
-
if (!loginList.includes(login)) {
|
|
508
|
-
throw new Error(`Team member ${login} in team ${team.name} is not registered in user list`);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
// Verify no duplicates in project names.
|
|
513
|
-
definition.projects.reduce((acc, project) => {
|
|
514
|
-
if (acc.includes(project.name)) {
|
|
515
|
-
throw new Error(`Duplicate project: ${project.name}`);
|
|
516
|
-
}
|
|
517
|
-
return [...acc, project.name];
|
|
518
|
-
}, []);
|
|
519
|
-
definition.projects.forEach((project) => {
|
|
520
|
-
project.github.forEach((org) => {
|
|
521
|
-
(org.teams || []).forEach((team) => {
|
|
522
|
-
const id = getTeamId(org.organization, team.name);
|
|
523
|
-
if (!teamIdList.includes(id)) {
|
|
524
|
-
throw new Error(`Project team ${id} in project ${project.name} is not registered in team list`);
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
(org.repos || []).forEach((repo) => {
|
|
528
|
-
(repo.teams || []).forEach((team) => {
|
|
529
|
-
const id = getTeamId(org.organization, team.name);
|
|
530
|
-
if (!teamIdList.includes(id)) {
|
|
531
|
-
throw new Error(`Repo team ${id} for repo ${repo.name} in project ${project.name} is not registered in team list`);
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
// Verify no duplicates in repos.
|
|
538
|
-
definition.projects
|
|
539
|
-
.flatMap((project) => project.github
|
|
540
|
-
.map((org) => (org.repos || []).map((repo) => getRepoId(org.organization, repo.name)))
|
|
541
|
-
.flat())
|
|
542
|
-
.reduce((acc, repoName) => {
|
|
543
|
-
if (acc.includes(repoName)) {
|
|
544
|
-
throw new Error(`Duplicate repo: ${repoName}`);
|
|
545
|
-
}
|
|
546
|
-
return [...acc, repoName];
|
|
547
|
-
}, []);
|
|
548
|
-
}
|
|
549
|
-
class DefinitionFile {
|
|
550
|
-
constructor(path) {
|
|
551
|
-
this.path = path;
|
|
552
|
-
}
|
|
553
|
-
async getContents() {
|
|
554
|
-
return new Promise((resolve, reject) => fs.readFile(this.path, "utf-8", (err, data) => {
|
|
555
|
-
if (err)
|
|
556
|
-
reject(err);
|
|
557
|
-
else
|
|
558
|
-
resolve(data);
|
|
559
|
-
}));
|
|
560
|
-
}
|
|
561
|
-
async getDefinition() {
|
|
562
|
-
return parseDefinition(await this.getContents());
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function parseDefinition(value) {
|
|
566
|
-
const result = checkAgainstSchema(yaml.load(value));
|
|
567
|
-
if ("error" in result) {
|
|
568
|
-
throw new Error("Definition content invalid: " + result.error);
|
|
569
|
-
}
|
|
570
|
-
requireValidDefinition(result.definition);
|
|
571
|
-
return result.definition;
|
|
572
|
-
}
|
|
573
|
-
function getRepos(definition) {
|
|
574
|
-
return definition.projects.flatMap((project) => project.github
|
|
575
|
-
.map((org) => (org.repos || []).map((repo) => ({
|
|
576
|
-
id: getRepoId(org.organization, repo.name),
|
|
577
|
-
orgName: org.organization,
|
|
578
|
-
project,
|
|
579
|
-
repo,
|
|
580
|
-
})))
|
|
581
|
-
.flat());
|
|
582
|
-
}
|
|
583
|
-
function getGitHubOrgs(definition) {
|
|
584
|
-
return uniq(definition.projects.flatMap((project) => project.github.map((it) => it.organization)));
|
|
469
|
+
function getTeamId(org, teamName) {
|
|
470
|
+
return `${org}/${teamName}`;
|
|
471
|
+
}
|
|
472
|
+
function getRepoId(orgName, repoName) {
|
|
473
|
+
return `${orgName}/${repoName}`;
|
|
474
|
+
}
|
|
475
|
+
function checkAgainstSchema(value) {
|
|
476
|
+
var _a;
|
|
477
|
+
const ajv = new AJV({ allErrors: true });
|
|
478
|
+
const valid = ajv.validate(schema, value);
|
|
479
|
+
return valid
|
|
480
|
+
? { definition: value }
|
|
481
|
+
: { error: (_a = ajv.errorsText()) !== null && _a !== void 0 ? _a : "Unknown error" };
|
|
482
|
+
}
|
|
483
|
+
function requireValidDefinition(definition) {
|
|
484
|
+
// Verify no duplicates in users and extract known logins.
|
|
485
|
+
const loginList = definition.github.users.reduce((acc, user) => {
|
|
486
|
+
if (acc.includes(user.login)) {
|
|
487
|
+
throw new Error(`Duplicate login: ${user.login}`);
|
|
488
|
+
}
|
|
489
|
+
return [...acc, user.login];
|
|
490
|
+
}, []);
|
|
491
|
+
// Verify no duplicates in teams and extract team names.
|
|
492
|
+
const teamIdList = definition.github.teams.reduce((acc, orgTeams) => {
|
|
493
|
+
return orgTeams.teams.reduce((acc1, team) => {
|
|
494
|
+
const id = getTeamId(orgTeams.organization, team.name);
|
|
495
|
+
if (acc1.includes(id)) {
|
|
496
|
+
throw new Error(`Duplicate team: ${id}`);
|
|
497
|
+
}
|
|
498
|
+
return [...acc1, id];
|
|
499
|
+
}, acc);
|
|
500
|
+
}, []);
|
|
501
|
+
// Verify team members exists as users.
|
|
502
|
+
definition.github.teams
|
|
503
|
+
.map((it) => it.teams)
|
|
504
|
+
.flat()
|
|
505
|
+
.forEach((team) => {
|
|
506
|
+
team.members.forEach((login) => {
|
|
507
|
+
if (!loginList.includes(login)) {
|
|
508
|
+
throw new Error(`Team member ${login} in team ${team.name} is not registered in user list`);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
// Verify no duplicates in project names.
|
|
513
|
+
definition.projects.reduce((acc, project) => {
|
|
514
|
+
if (acc.includes(project.name)) {
|
|
515
|
+
throw new Error(`Duplicate project: ${project.name}`);
|
|
516
|
+
}
|
|
517
|
+
return [...acc, project.name];
|
|
518
|
+
}, []);
|
|
519
|
+
definition.projects.forEach((project) => {
|
|
520
|
+
project.github.forEach((org) => {
|
|
521
|
+
(org.teams || []).forEach((team) => {
|
|
522
|
+
const id = getTeamId(org.organization, team.name);
|
|
523
|
+
if (!teamIdList.includes(id)) {
|
|
524
|
+
throw new Error(`Project team ${id} in project ${project.name} is not registered in team list`);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
(org.repos || []).forEach((repo) => {
|
|
528
|
+
(repo.teams || []).forEach((team) => {
|
|
529
|
+
const id = getTeamId(org.organization, team.name);
|
|
530
|
+
if (!teamIdList.includes(id)) {
|
|
531
|
+
throw new Error(`Repo team ${id} for repo ${repo.name} in project ${project.name} is not registered in team list`);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
// Verify no duplicates in repos.
|
|
538
|
+
definition.projects
|
|
539
|
+
.flatMap((project) => project.github
|
|
540
|
+
.map((org) => (org.repos || []).map((repo) => getRepoId(org.organization, repo.name)))
|
|
541
|
+
.flat())
|
|
542
|
+
.reduce((acc, repoName) => {
|
|
543
|
+
if (acc.includes(repoName)) {
|
|
544
|
+
throw new Error(`Duplicate repo: ${repoName}`);
|
|
545
|
+
}
|
|
546
|
+
return [...acc, repoName];
|
|
547
|
+
}, []);
|
|
548
|
+
}
|
|
549
|
+
class DefinitionFile {
|
|
550
|
+
constructor(path) {
|
|
551
|
+
this.path = path;
|
|
552
|
+
}
|
|
553
|
+
async getContents() {
|
|
554
|
+
return new Promise((resolve, reject) => fs.readFile(this.path, "utf-8", (err, data) => {
|
|
555
|
+
if (err)
|
|
556
|
+
reject(err);
|
|
557
|
+
else
|
|
558
|
+
resolve(data);
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
async getDefinition() {
|
|
562
|
+
return parseDefinition(await this.getContents());
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function parseDefinition(value) {
|
|
566
|
+
const result = checkAgainstSchema(yaml.load(value));
|
|
567
|
+
if ("error" in result) {
|
|
568
|
+
throw new Error("Definition content invalid: " + result.error);
|
|
569
|
+
}
|
|
570
|
+
requireValidDefinition(result.definition);
|
|
571
|
+
return result.definition;
|
|
572
|
+
}
|
|
573
|
+
function getRepos(definition) {
|
|
574
|
+
return definition.projects.flatMap((project) => project.github
|
|
575
|
+
.map((org) => (org.repos || []).map((repo) => ({
|
|
576
|
+
id: getRepoId(org.organization, repo.name),
|
|
577
|
+
orgName: org.organization,
|
|
578
|
+
project,
|
|
579
|
+
repo,
|
|
580
|
+
})))
|
|
581
|
+
.flat());
|
|
582
|
+
}
|
|
583
|
+
function getGitHubOrgs(definition) {
|
|
584
|
+
return uniq(definition.projects.flatMap((project) => project.github.map((it) => it.organization)));
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
-
function createReporter(argv) {
|
|
588
|
-
return new Reporter({
|
|
589
|
-
verbose: !!argv.verbose,
|
|
590
|
-
nonInteractive: !!argv.nonInteractive,
|
|
591
|
-
});
|
|
587
|
+
function createReporter(argv) {
|
|
588
|
+
return new Reporter({
|
|
589
|
+
verbose: !!argv.verbose,
|
|
590
|
+
nonInteractive: !!argv.nonInteractive,
|
|
591
|
+
});
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
class GitHubTokenCliProvider {
|
|
595
|
-
constructor() {
|
|
596
|
-
this.keyringService = "cals";
|
|
597
|
-
this.keyringAccount = "github-token";
|
|
598
|
-
}
|
|
599
|
-
async getToken() {
|
|
600
|
-
if (process.env.CALS_GITHUB_TOKEN) {
|
|
601
|
-
return process.env.CALS_GITHUB_TOKEN;
|
|
602
|
-
}
|
|
603
|
-
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
604
|
-
if (result == null) {
|
|
605
|
-
process.stderr.write("No token found. Register using `cals github set-token`\n");
|
|
606
|
-
return undefined;
|
|
607
|
-
}
|
|
608
|
-
return result;
|
|
609
|
-
}
|
|
610
|
-
async markInvalid() {
|
|
611
|
-
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
612
|
-
}
|
|
613
|
-
async setToken(value) {
|
|
614
|
-
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
615
|
-
}
|
|
594
|
+
class GitHubTokenCliProvider {
|
|
595
|
+
constructor() {
|
|
596
|
+
this.keyringService = "cals";
|
|
597
|
+
this.keyringAccount = "github-token";
|
|
598
|
+
}
|
|
599
|
+
async getToken() {
|
|
600
|
+
if (process.env.CALS_GITHUB_TOKEN) {
|
|
601
|
+
return process.env.CALS_GITHUB_TOKEN;
|
|
602
|
+
}
|
|
603
|
+
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
604
|
+
if (result == null) {
|
|
605
|
+
process.stderr.write("No token found. Register using `cals github set-token`\n");
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
async markInvalid() {
|
|
611
|
+
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
612
|
+
}
|
|
613
|
+
async setToken(value) {
|
|
614
|
+
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
615
|
+
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
async function undefinedForNotFound(value) {
|
|
619
|
-
try {
|
|
620
|
-
return await value;
|
|
621
|
-
}
|
|
622
|
-
catch (e) {
|
|
623
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
624
|
-
if (e.name === "HttpError" && e.status === 404) {
|
|
625
|
-
return undefined;
|
|
626
|
-
}
|
|
627
|
-
else {
|
|
628
|
-
throw e;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
618
|
+
async function undefinedForNotFound(value) {
|
|
619
|
+
try {
|
|
620
|
+
return await value;
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
624
|
+
if (e.name === "HttpError" && e.status === 404) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
throw e;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
class GitHubService {
|
|
634
|
-
constructor(props) {
|
|
635
|
-
this._requestCount = 0;
|
|
636
|
-
this.config = props.config;
|
|
637
|
-
this.octokit = props.octokit;
|
|
638
|
-
this.cache = props.cache;
|
|
639
|
-
this.tokenProvider = props.tokenProvider;
|
|
640
|
-
// Control concurrency to GitHub API at service level so we
|
|
641
|
-
// can maximize concurrency all other places.
|
|
642
|
-
this.semaphore = pLimit(6);
|
|
643
|
-
this.octokit.hook.wrap("request", async (request, options) => {
|
|
644
|
-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
645
|
-
this._requestCount++;
|
|
646
|
-
if (options.method !== "GET") {
|
|
647
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
648
|
-
return this.semaphore(() => request(options));
|
|
649
|
-
}
|
|
650
|
-
// Try to cache ETag for GET requests to save on rate limiting.
|
|
651
|
-
// Hits on ETag does not count towards rate limiting.
|
|
652
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
653
|
-
const rest = {
|
|
654
|
-
...options,
|
|
655
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
656
|
-
};
|
|
657
|
-
delete rest.method;
|
|
658
|
-
delete rest.baseUrl;
|
|
659
|
-
delete rest.headers;
|
|
660
|
-
delete rest.mediaType;
|
|
661
|
-
delete rest.request;
|
|
662
|
-
// Build a key that is used to identify this request.
|
|
663
|
-
const key = Buffer.from(JSON.stringify(rest)).toString("base64");
|
|
664
|
-
const cacheItem = this.cache.retrieveJson(key);
|
|
665
|
-
if (cacheItem !== undefined) {
|
|
666
|
-
// Copying doesn't work, seems we need to mutate this.
|
|
667
|
-
options.headers["If-None-Match"] = cacheItem.data.etag;
|
|
668
|
-
}
|
|
669
|
-
const getResponse = async (allowRetry = true) => {
|
|
670
|
-
try {
|
|
671
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
672
|
-
return await request(options);
|
|
673
|
-
}
|
|
674
|
-
catch (e) {
|
|
675
|
-
// Handle no change in ETag.
|
|
676
|
-
if (e.status === 304) {
|
|
677
|
-
return undefined;
|
|
678
|
-
}
|
|
679
|
-
// GitHub seems to throw a lot of 502 errors.
|
|
680
|
-
// Let's give it a few seconds and retry one time.
|
|
681
|
-
if (e.status === 502 && allowRetry) {
|
|
682
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
683
|
-
return await getResponse(false);
|
|
684
|
-
}
|
|
685
|
-
throw e;
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
const response = await this.semaphore(async () => {
|
|
689
|
-
return getResponse();
|
|
690
|
-
});
|
|
691
|
-
if (response === undefined) {
|
|
692
|
-
// Undefined is returned for cached data.
|
|
693
|
-
if (cacheItem === undefined) {
|
|
694
|
-
throw new Error("Missing expected cache item");
|
|
695
|
-
}
|
|
696
|
-
// Use previous value.
|
|
697
|
-
return cacheItem.data.data;
|
|
698
|
-
}
|
|
699
|
-
// New value. Store Etag.
|
|
700
|
-
if (response.headers.etag) {
|
|
701
|
-
this.cache.storeJson(key, {
|
|
702
|
-
etag: response.headers.etag,
|
|
703
|
-
data: response,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
return response;
|
|
707
|
-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
get requestCount() {
|
|
711
|
-
return this._requestCount;
|
|
712
|
-
}
|
|
713
|
-
async runGraphqlQuery(query) {
|
|
714
|
-
const token = await this.tokenProvider.getToken();
|
|
715
|
-
if (token === undefined) {
|
|
716
|
-
throw new Error("Missing token for GitHub");
|
|
717
|
-
}
|
|
718
|
-
const url = "https://api.github.com/graphql";
|
|
719
|
-
const headers = {
|
|
720
|
-
Authorization: `Bearer ${token}`,
|
|
721
|
-
};
|
|
722
|
-
const response = await this.semaphore(() => fetch(url, {
|
|
723
|
-
method: "POST",
|
|
724
|
-
headers,
|
|
725
|
-
body: JSON.stringify({ query }),
|
|
726
|
-
agent: this.config.agent,
|
|
727
|
-
}));
|
|
728
|
-
if (response.status === 401) {
|
|
729
|
-
process.stderr.write("Unauthorized\n");
|
|
730
|
-
await this.tokenProvider.markInvalid();
|
|
731
|
-
}
|
|
732
|
-
if (!response.ok) {
|
|
733
|
-
throw new Error(`Response from GitHub not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
734
|
-
}
|
|
735
|
-
const json = (await response.json());
|
|
736
|
-
if (!!json.errors) {
|
|
737
|
-
throw new Error(`Error from GitHub GraphQL API: ${JSON.stringify(json.errors)}`);
|
|
738
|
-
}
|
|
739
|
-
if (json.data == null) {
|
|
740
|
-
throw new Error(`No data received from GitHub GraphQL API (unknown reason)`);
|
|
741
|
-
}
|
|
742
|
-
return json.data;
|
|
743
|
-
}
|
|
744
|
-
async getOrgRepoList({ org }) {
|
|
633
|
+
class GitHubService {
|
|
634
|
+
constructor(props) {
|
|
635
|
+
this._requestCount = 0;
|
|
636
|
+
this.config = props.config;
|
|
637
|
+
this.octokit = props.octokit;
|
|
638
|
+
this.cache = props.cache;
|
|
639
|
+
this.tokenProvider = props.tokenProvider;
|
|
640
|
+
// Control concurrency to GitHub API at service level so we
|
|
641
|
+
// can maximize concurrency all other places.
|
|
642
|
+
this.semaphore = pLimit(6);
|
|
643
|
+
this.octokit.hook.wrap("request", async (request, options) => {
|
|
644
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
645
|
+
this._requestCount++;
|
|
646
|
+
if (options.method !== "GET") {
|
|
647
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
648
|
+
return this.semaphore(() => request(options));
|
|
649
|
+
}
|
|
650
|
+
// Try to cache ETag for GET requests to save on rate limiting.
|
|
651
|
+
// Hits on ETag does not count towards rate limiting.
|
|
652
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
653
|
+
const rest = {
|
|
654
|
+
...options,
|
|
655
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
656
|
+
};
|
|
657
|
+
delete rest.method;
|
|
658
|
+
delete rest.baseUrl;
|
|
659
|
+
delete rest.headers;
|
|
660
|
+
delete rest.mediaType;
|
|
661
|
+
delete rest.request;
|
|
662
|
+
// Build a key that is used to identify this request.
|
|
663
|
+
const key = Buffer.from(JSON.stringify(rest)).toString("base64");
|
|
664
|
+
const cacheItem = this.cache.retrieveJson(key);
|
|
665
|
+
if (cacheItem !== undefined) {
|
|
666
|
+
// Copying doesn't work, seems we need to mutate this.
|
|
667
|
+
options.headers["If-None-Match"] = cacheItem.data.etag;
|
|
668
|
+
}
|
|
669
|
+
const getResponse = async (allowRetry = true) => {
|
|
670
|
+
try {
|
|
671
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
672
|
+
return await request(options);
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
// Handle no change in ETag.
|
|
676
|
+
if (e.status === 304) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
// GitHub seems to throw a lot of 502 errors.
|
|
680
|
+
// Let's give it a few seconds and retry one time.
|
|
681
|
+
if (e.status === 502 && allowRetry) {
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
683
|
+
return await getResponse(false);
|
|
684
|
+
}
|
|
685
|
+
throw e;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const response = await this.semaphore(async () => {
|
|
689
|
+
return getResponse();
|
|
690
|
+
});
|
|
691
|
+
if (response === undefined) {
|
|
692
|
+
// Undefined is returned for cached data.
|
|
693
|
+
if (cacheItem === undefined) {
|
|
694
|
+
throw new Error("Missing expected cache item");
|
|
695
|
+
}
|
|
696
|
+
// Use previous value.
|
|
697
|
+
return cacheItem.data.data;
|
|
698
|
+
}
|
|
699
|
+
// New value. Store Etag.
|
|
700
|
+
if (response.headers.etag) {
|
|
701
|
+
this.cache.storeJson(key, {
|
|
702
|
+
etag: response.headers.etag,
|
|
703
|
+
data: response,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return response;
|
|
707
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
get requestCount() {
|
|
711
|
+
return this._requestCount;
|
|
712
|
+
}
|
|
713
|
+
async runGraphqlQuery(query) {
|
|
714
|
+
const token = await this.tokenProvider.getToken();
|
|
715
|
+
if (token === undefined) {
|
|
716
|
+
throw new Error("Missing token for GitHub");
|
|
717
|
+
}
|
|
718
|
+
const url = "https://api.github.com/graphql";
|
|
719
|
+
const headers = {
|
|
720
|
+
Authorization: `Bearer ${token}`,
|
|
721
|
+
};
|
|
722
|
+
const response = await this.semaphore(() => fetch(url, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers,
|
|
725
|
+
body: JSON.stringify({ query }),
|
|
726
|
+
agent: this.config.agent,
|
|
727
|
+
}));
|
|
728
|
+
if (response.status === 401) {
|
|
729
|
+
process.stderr.write("Unauthorized\n");
|
|
730
|
+
await this.tokenProvider.markInvalid();
|
|
731
|
+
}
|
|
732
|
+
if (!response.ok) {
|
|
733
|
+
throw new Error(`Response from GitHub not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
734
|
+
}
|
|
735
|
+
const json = (await response.json());
|
|
736
|
+
if (!!json.errors) {
|
|
737
|
+
throw new Error(`Error from GitHub GraphQL API: ${JSON.stringify(json.errors)}`);
|
|
738
|
+
}
|
|
739
|
+
if (json.data == null) {
|
|
740
|
+
throw new Error(`No data received from GitHub GraphQL API (unknown reason)`);
|
|
741
|
+
}
|
|
742
|
+
return json.data;
|
|
743
|
+
}
|
|
744
|
+
async getOrgRepoList({ org }) {
|
|
745
745
|
const getQuery = (after) => `{
|
|
746
746
|
organization(login: "${org}") {
|
|
747
747
|
repositories(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
@@ -774,144 +774,144 @@ class GitHubService {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
}
|
|
777
|
-
}`;
|
|
778
|
-
return this.cache.json(`repos-${org}`, async () => {
|
|
779
|
-
const repos = [];
|
|
780
|
-
let after = null;
|
|
781
|
-
while (true) {
|
|
782
|
-
const query = getQuery(after);
|
|
783
|
-
const res = await this.runGraphqlQuery(query);
|
|
784
|
-
if (res.organization == null) {
|
|
785
|
-
throw new Error("Missing organization");
|
|
786
|
-
}
|
|
787
|
-
if (res.organization.repositories.nodes == null) {
|
|
788
|
-
throw new Error("Missing organization nodes");
|
|
789
|
-
}
|
|
790
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
791
|
-
repos.push(...res.organization.repositories.nodes);
|
|
792
|
-
if (!res.organization.repositories.pageInfo.hasNextPage) {
|
|
793
|
-
break;
|
|
794
|
-
}
|
|
795
|
-
after = res.organization.repositories.pageInfo.endCursor;
|
|
796
|
-
}
|
|
797
|
-
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
async getOrgMembersList(org) {
|
|
801
|
-
const options = this.octokit.orgs.listMembers.endpoint.merge({
|
|
802
|
-
org,
|
|
803
|
-
});
|
|
804
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
805
|
-
}
|
|
806
|
-
async getOrgMembersInvitedList(org) {
|
|
807
|
-
const options = this.octokit.orgs.listPendingInvitations.endpoint.merge({
|
|
808
|
-
org,
|
|
809
|
-
});
|
|
810
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
811
|
-
}
|
|
812
|
-
async getOrgMembersListIncludingInvited(org) {
|
|
813
|
-
return [
|
|
814
|
-
...(await this.getOrgMembersList(org)).map((it) => ({
|
|
815
|
-
type: "member",
|
|
816
|
-
login: it.login,
|
|
817
|
-
data: it,
|
|
818
|
-
})),
|
|
819
|
-
...(await this.getOrgMembersInvitedList(org)).map((it) => {
|
|
820
|
-
var _a;
|
|
821
|
-
return ({
|
|
822
|
-
type: "invited",
|
|
823
|
-
// TODO: Fix ?? case properly
|
|
824
|
-
login: (_a = it.login) !== null && _a !== void 0 ? _a : "invalid",
|
|
825
|
-
data: it,
|
|
826
|
-
});
|
|
827
|
-
}),
|
|
828
|
-
];
|
|
829
|
-
}
|
|
830
|
-
async getRepository(owner, repo) {
|
|
831
|
-
return this.cache.json(`get-repository-${owner}-${repo}`, async () => {
|
|
832
|
-
const response = await undefinedForNotFound(this.octokit.repos.get({
|
|
833
|
-
owner,
|
|
834
|
-
repo,
|
|
835
|
-
}));
|
|
836
|
-
return response === undefined ? undefined : response.data;
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
async getRepositoryTeamsList(repo) {
|
|
840
|
-
return this.cache.json(`repository-teams-list-${repo.id}`, async () => {
|
|
841
|
-
const options = this.octokit.repos.listTeams.endpoint.merge({
|
|
842
|
-
owner: repo.owner.login,
|
|
843
|
-
repo: repo.name,
|
|
844
|
-
});
|
|
845
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
async getRepositoryHooks(owner, repo) {
|
|
849
|
-
return this.cache.json(`repository-hooks-${owner}-${repo}`, async () => {
|
|
850
|
-
const options = this.octokit.repos.listWebhooks.endpoint.merge({
|
|
851
|
-
owner,
|
|
852
|
-
repo,
|
|
853
|
-
});
|
|
854
|
-
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
async getOrg(org) {
|
|
858
|
-
const orgResponse = await this.octokit.orgs.get({
|
|
859
|
-
org,
|
|
860
|
-
});
|
|
861
|
-
return orgResponse.data;
|
|
862
|
-
}
|
|
863
|
-
async getTeamList(org) {
|
|
864
|
-
return this.cache.json(`team-list-${org.login}`, async () => {
|
|
865
|
-
const options = this.octokit.teams.list.endpoint.merge({
|
|
866
|
-
org: org.login,
|
|
867
|
-
});
|
|
868
|
-
return (await this.octokit.paginate(options));
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
async getTeamMemberList(org, team) {
|
|
872
|
-
return this.cache.json(`team-member-list-${team.id}`, async () => {
|
|
873
|
-
const options = this.octokit.teams.listMembersInOrg.endpoint.merge({
|
|
874
|
-
org: org.login,
|
|
875
|
-
team_slug: team.slug,
|
|
876
|
-
});
|
|
877
|
-
return (await this.octokit.paginate(options));
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
async getTeamMemberInvitedList(org, team) {
|
|
881
|
-
return this.cache.json(`team-member-invited-list-${team.id}`, async () => {
|
|
882
|
-
const options = this.octokit.teams.listPendingInvitationsInOrg.endpoint.merge({
|
|
883
|
-
org: org.login,
|
|
884
|
-
team_slug: team.slug,
|
|
885
|
-
});
|
|
886
|
-
return (await this.octokit.paginate(options));
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
async getTeamMemberListIncludingInvited(org, team) {
|
|
890
|
-
return [
|
|
891
|
-
...(await this.getTeamMemberList(org, team)).map((it) => ({
|
|
892
|
-
type: "member",
|
|
893
|
-
login: it.login,
|
|
894
|
-
data: it,
|
|
895
|
-
})),
|
|
896
|
-
...(await this.getTeamMemberInvitedList(org, team)).map((it) => {
|
|
897
|
-
var _a;
|
|
898
|
-
return ({
|
|
899
|
-
type: "invited",
|
|
900
|
-
// TODO: Fix ?? case properly
|
|
901
|
-
login: (_a = it.login) !== null && _a !== void 0 ? _a : "invalid",
|
|
902
|
-
data: it,
|
|
903
|
-
});
|
|
904
|
-
}),
|
|
905
|
-
];
|
|
906
|
-
}
|
|
907
|
-
async getSearchedPullRequestList() {
|
|
908
|
-
// NOTE: Changes to this must by synced with SearchedPullRequestListQueryResult.
|
|
777
|
+
}`;
|
|
778
|
+
return this.cache.json(`repos-${org}`, async () => {
|
|
779
|
+
const repos = [];
|
|
780
|
+
let after = null;
|
|
781
|
+
while (true) {
|
|
782
|
+
const query = getQuery(after);
|
|
783
|
+
const res = await this.runGraphqlQuery(query);
|
|
784
|
+
if (res.organization == null) {
|
|
785
|
+
throw new Error("Missing organization");
|
|
786
|
+
}
|
|
787
|
+
if (res.organization.repositories.nodes == null) {
|
|
788
|
+
throw new Error("Missing organization nodes");
|
|
789
|
+
}
|
|
790
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
791
|
+
repos.push(...res.organization.repositories.nodes);
|
|
792
|
+
if (!res.organization.repositories.pageInfo.hasNextPage) {
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
after = res.organization.repositories.pageInfo.endCursor;
|
|
796
|
+
}
|
|
797
|
+
return repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
async getOrgMembersList(org) {
|
|
801
|
+
const options = this.octokit.orgs.listMembers.endpoint.merge({
|
|
802
|
+
org,
|
|
803
|
+
});
|
|
804
|
+
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
805
|
+
}
|
|
806
|
+
async getOrgMembersInvitedList(org) {
|
|
807
|
+
const options = this.octokit.orgs.listPendingInvitations.endpoint.merge({
|
|
808
|
+
org,
|
|
809
|
+
});
|
|
810
|
+
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
811
|
+
}
|
|
812
|
+
async getOrgMembersListIncludingInvited(org) {
|
|
813
|
+
return [
|
|
814
|
+
...(await this.getOrgMembersList(org)).map((it) => ({
|
|
815
|
+
type: "member",
|
|
816
|
+
login: it.login,
|
|
817
|
+
data: it,
|
|
818
|
+
})),
|
|
819
|
+
...(await this.getOrgMembersInvitedList(org)).map((it) => {
|
|
820
|
+
var _a;
|
|
821
|
+
return ({
|
|
822
|
+
type: "invited",
|
|
823
|
+
// TODO: Fix ?? case properly
|
|
824
|
+
login: (_a = it.login) !== null && _a !== void 0 ? _a : "invalid",
|
|
825
|
+
data: it,
|
|
826
|
+
});
|
|
827
|
+
}),
|
|
828
|
+
];
|
|
829
|
+
}
|
|
830
|
+
async getRepository(owner, repo) {
|
|
831
|
+
return this.cache.json(`get-repository-${owner}-${repo}`, async () => {
|
|
832
|
+
const response = await undefinedForNotFound(this.octokit.repos.get({
|
|
833
|
+
owner,
|
|
834
|
+
repo,
|
|
835
|
+
}));
|
|
836
|
+
return response === undefined ? undefined : response.data;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async getRepositoryTeamsList(repo) {
|
|
840
|
+
return this.cache.json(`repository-teams-list-${repo.id}`, async () => {
|
|
841
|
+
const options = this.octokit.repos.listTeams.endpoint.merge({
|
|
842
|
+
owner: repo.owner.login,
|
|
843
|
+
repo: repo.name,
|
|
844
|
+
});
|
|
845
|
+
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async getRepositoryHooks(owner, repo) {
|
|
849
|
+
return this.cache.json(`repository-hooks-${owner}-${repo}`, async () => {
|
|
850
|
+
const options = this.octokit.repos.listWebhooks.endpoint.merge({
|
|
851
|
+
owner,
|
|
852
|
+
repo,
|
|
853
|
+
});
|
|
854
|
+
return ((await undefinedForNotFound(this.octokit.paginate(options))) || []);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async getOrg(org) {
|
|
858
|
+
const orgResponse = await this.octokit.orgs.get({
|
|
859
|
+
org,
|
|
860
|
+
});
|
|
861
|
+
return orgResponse.data;
|
|
862
|
+
}
|
|
863
|
+
async getTeamList(org) {
|
|
864
|
+
return this.cache.json(`team-list-${org.login}`, async () => {
|
|
865
|
+
const options = this.octokit.teams.list.endpoint.merge({
|
|
866
|
+
org: org.login,
|
|
867
|
+
});
|
|
868
|
+
return (await this.octokit.paginate(options));
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
async getTeamMemberList(org, team) {
|
|
872
|
+
return this.cache.json(`team-member-list-${team.id}`, async () => {
|
|
873
|
+
const options = this.octokit.teams.listMembersInOrg.endpoint.merge({
|
|
874
|
+
org: org.login,
|
|
875
|
+
team_slug: team.slug,
|
|
876
|
+
});
|
|
877
|
+
return (await this.octokit.paginate(options));
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async getTeamMemberInvitedList(org, team) {
|
|
881
|
+
return this.cache.json(`team-member-invited-list-${team.id}`, async () => {
|
|
882
|
+
const options = this.octokit.teams.listPendingInvitationsInOrg.endpoint.merge({
|
|
883
|
+
org: org.login,
|
|
884
|
+
team_slug: team.slug,
|
|
885
|
+
});
|
|
886
|
+
return (await this.octokit.paginate(options));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
async getTeamMemberListIncludingInvited(org, team) {
|
|
890
|
+
return [
|
|
891
|
+
...(await this.getTeamMemberList(org, team)).map((it) => ({
|
|
892
|
+
type: "member",
|
|
893
|
+
login: it.login,
|
|
894
|
+
data: it,
|
|
895
|
+
})),
|
|
896
|
+
...(await this.getTeamMemberInvitedList(org, team)).map((it) => {
|
|
897
|
+
var _a;
|
|
898
|
+
return ({
|
|
899
|
+
type: "invited",
|
|
900
|
+
// TODO: Fix ?? case properly
|
|
901
|
+
login: (_a = it.login) !== null && _a !== void 0 ? _a : "invalid",
|
|
902
|
+
data: it,
|
|
903
|
+
});
|
|
904
|
+
}),
|
|
905
|
+
];
|
|
906
|
+
}
|
|
907
|
+
async getSearchedPullRequestList() {
|
|
908
|
+
// NOTE: Changes to this must by synced with SearchedPullRequestListQueryResult.
|
|
909
909
|
const getQuery = (after) => `{
|
|
910
910
|
search(
|
|
911
911
|
query: "is:open is:pr user:capralifecycle user:capraconsulting archived:false",
|
|
912
912
|
type: ISSUE,
|
|
913
|
-
first: 100${after === null
|
|
914
|
-
? ""
|
|
913
|
+
first: 100${after === null
|
|
914
|
+
? ""
|
|
915
915
|
: `,
|
|
916
916
|
after: "${after}"`}
|
|
917
917
|
) {
|
|
@@ -950,51 +950,51 @@ class GitHubService {
|
|
|
950
950
|
}
|
|
951
951
|
}
|
|
952
952
|
}
|
|
953
|
-
}`;
|
|
954
|
-
const pulls = [];
|
|
955
|
-
let after = null;
|
|
956
|
-
while (true) {
|
|
957
|
-
const query = getQuery(after);
|
|
958
|
-
const res = await this.runGraphqlQuery(query);
|
|
959
|
-
pulls.push(...res.search.edges.map((it) => it.node));
|
|
960
|
-
if (!res.search.pageInfo.hasNextPage) {
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
after = res.search.pageInfo.endCursor;
|
|
964
|
-
}
|
|
965
|
-
return pulls.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
966
|
-
}
|
|
967
|
-
async getHasVulnerabilityAlertsEnabled(owner, repo) {
|
|
968
|
-
try {
|
|
969
|
-
const response = await this.octokit.repos.checkVulnerabilityAlerts({
|
|
970
|
-
owner: owner,
|
|
971
|
-
repo: repo,
|
|
972
|
-
});
|
|
973
|
-
if (response.status !== 204) {
|
|
974
|
-
console.log(response);
|
|
975
|
-
throw new Error("Unknown response - see previous log line");
|
|
976
|
-
}
|
|
977
|
-
return true;
|
|
978
|
-
}
|
|
979
|
-
catch (e) {
|
|
980
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
981
|
-
if (e.status === 404) {
|
|
982
|
-
return false;
|
|
983
|
-
}
|
|
984
|
-
throw e;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
async enableVulnerabilityAlerts(owner, repo) {
|
|
988
|
-
await this.octokit.repos.enableVulnerabilityAlerts({
|
|
989
|
-
owner: owner,
|
|
990
|
-
repo: repo,
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Get the vulnerability alerts for a repository.
|
|
995
|
-
*/
|
|
996
|
-
async getVulnerabilityAlerts(owner, repo) {
|
|
997
|
-
// NOTE: Changes to this must by synced with VulnerabilityAlertsQueryResult.
|
|
953
|
+
}`;
|
|
954
|
+
const pulls = [];
|
|
955
|
+
let after = null;
|
|
956
|
+
while (true) {
|
|
957
|
+
const query = getQuery(after);
|
|
958
|
+
const res = await this.runGraphqlQuery(query);
|
|
959
|
+
pulls.push(...res.search.edges.map((it) => it.node));
|
|
960
|
+
if (!res.search.pageInfo.hasNextPage) {
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
after = res.search.pageInfo.endCursor;
|
|
964
|
+
}
|
|
965
|
+
return pulls.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
966
|
+
}
|
|
967
|
+
async getHasVulnerabilityAlertsEnabled(owner, repo) {
|
|
968
|
+
try {
|
|
969
|
+
const response = await this.octokit.repos.checkVulnerabilityAlerts({
|
|
970
|
+
owner: owner,
|
|
971
|
+
repo: repo,
|
|
972
|
+
});
|
|
973
|
+
if (response.status !== 204) {
|
|
974
|
+
console.log(response);
|
|
975
|
+
throw new Error("Unknown response - see previous log line");
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
981
|
+
if (e.status === 404) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
throw e;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
async enableVulnerabilityAlerts(owner, repo) {
|
|
988
|
+
await this.octokit.repos.enableVulnerabilityAlerts({
|
|
989
|
+
owner: owner,
|
|
990
|
+
repo: repo,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get the vulnerability alerts for a repository.
|
|
995
|
+
*/
|
|
996
|
+
async getVulnerabilityAlerts(owner, repo) {
|
|
997
|
+
// NOTE: Changes to this must by synced with VulnerabilityAlertsQueryResult.
|
|
998
998
|
const getQuery = (after) => `{
|
|
999
999
|
repository(owner: "${owner}", name: "${repo}") {
|
|
1000
1000
|
vulnerabilityAlerts(first: 100${after === null ? "" : `, after: "${after}"`}) {
|
|
@@ -1024,28 +1024,28 @@ class GitHubService {
|
|
|
1024
1024
|
}
|
|
1025
1025
|
}
|
|
1026
1026
|
}
|
|
1027
|
-
}`;
|
|
1028
|
-
return this.cache.json(`vulnerability-alerts-${owner}-${repo}`, async () => {
|
|
1029
|
-
var _a, _b, _c, _d, _e;
|
|
1030
|
-
const result = [];
|
|
1031
|
-
let after = null;
|
|
1032
|
-
while (true) {
|
|
1033
|
-
const query = getQuery(after);
|
|
1034
|
-
const res = await this.runGraphqlQuery(query);
|
|
1035
|
-
result.push(...((_c = (_b = (_a = res.repository) === null || _a === void 0 ? void 0 : _a.vulnerabilityAlerts.edges) === null || _b === void 0 ? void 0 : _b.map((it) => it.node)) !== null && _c !== void 0 ? _c : []));
|
|
1036
|
-
if (!((_d = res.repository) === null || _d === void 0 ? void 0 : _d.vulnerabilityAlerts.pageInfo.hasNextPage)) {
|
|
1037
|
-
break;
|
|
1038
|
-
}
|
|
1039
|
-
after = (_e = res.repository) === null || _e === void 0 ? void 0 : _e.vulnerabilityAlerts.pageInfo.endCursor;
|
|
1040
|
-
}
|
|
1041
|
-
return result;
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Get the Renovate Dependency Dashboard issue.
|
|
1046
|
-
*/
|
|
1047
|
-
async getRenovateDependencyDashboardIssue(owner, repo) {
|
|
1048
|
-
// NOTE: Changes to this must by synced with RenovateDependencyDashboardIssueQueryResult.
|
|
1027
|
+
}`;
|
|
1028
|
+
return this.cache.json(`vulnerability-alerts-${owner}-${repo}`, async () => {
|
|
1029
|
+
var _a, _b, _c, _d, _e;
|
|
1030
|
+
const result = [];
|
|
1031
|
+
let after = null;
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const query = getQuery(after);
|
|
1034
|
+
const res = await this.runGraphqlQuery(query);
|
|
1035
|
+
result.push(...((_c = (_b = (_a = res.repository) === null || _a === void 0 ? void 0 : _a.vulnerabilityAlerts.edges) === null || _b === void 0 ? void 0 : _b.map((it) => it.node)) !== null && _c !== void 0 ? _c : []));
|
|
1036
|
+
if (!((_d = res.repository) === null || _d === void 0 ? void 0 : _d.vulnerabilityAlerts.pageInfo.hasNextPage)) {
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
after = (_e = res.repository) === null || _e === void 0 ? void 0 : _e.vulnerabilityAlerts.pageInfo.endCursor;
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Get the Renovate Dependency Dashboard issue.
|
|
1046
|
+
*/
|
|
1047
|
+
async getRenovateDependencyDashboardIssue(owner, repo) {
|
|
1048
|
+
// NOTE: Changes to this must by synced with RenovateDependencyDashboardIssueQueryResult.
|
|
1049
1049
|
const getQuery = (after) => `{
|
|
1050
1050
|
repository(owner: "${owner}", name: "${repo}") {
|
|
1051
1051
|
issues(
|
|
@@ -1076,55 +1076,55 @@ class GitHubService {
|
|
|
1076
1076
|
}
|
|
1077
1077
|
}
|
|
1078
1078
|
}
|
|
1079
|
-
}`;
|
|
1080
|
-
const issues = await this.cache.json(`renovate-bot-issues-${owner}-${repo}`, async () => {
|
|
1081
|
-
var _a, _b, _c, _d, _e;
|
|
1082
|
-
const result = [];
|
|
1083
|
-
let after = null;
|
|
1084
|
-
while (true) {
|
|
1085
|
-
const query = getQuery(after);
|
|
1086
|
-
const res = await this.runGraphqlQuery(query);
|
|
1087
|
-
const nodes = (_c = (_b = (_a = res.repository) === null || _a === void 0 ? void 0 : _a.issues.edges) === null || _b === void 0 ? void 0 : _b.map((it) => it.node)) !== null && _c !== void 0 ? _c : [];
|
|
1088
|
-
result.push(...nodes
|
|
1089
|
-
.filter((it) => it.title === "Dependency Dashboard")
|
|
1090
|
-
.map((it) => {
|
|
1091
|
-
var _a, _b, _c, _d, _e;
|
|
1092
|
-
return ({
|
|
1093
|
-
number: it.number,
|
|
1094
|
-
body: it.body,
|
|
1095
|
-
lastUpdatedByRenovate: (_e = (_d = (_c = (_b = (_a = it.userContentEdits) === null || _a === void 0 ? void 0 : _a.nodes) === null || _b === void 0 ? void 0 : _b.filter((it) => { var _a; return ((_a = it.editor) === null || _a === void 0 ? void 0 : _a.login) === "renovate"; })) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.createdAt) !== null && _e !== void 0 ? _e : null,
|
|
1096
|
-
});
|
|
1097
|
-
}));
|
|
1098
|
-
if (!((_d = res.repository) === null || _d === void 0 ? void 0 : _d.issues.pageInfo.hasNextPage)) {
|
|
1099
|
-
break;
|
|
1100
|
-
}
|
|
1101
|
-
after = (_e = res.repository) === null || _e === void 0 ? void 0 : _e.issues.pageInfo.endCursor;
|
|
1102
|
-
}
|
|
1103
|
-
return result;
|
|
1104
|
-
});
|
|
1105
|
-
if (issues.length == 0) {
|
|
1106
|
-
return undefined;
|
|
1107
|
-
}
|
|
1108
|
-
return issues[0];
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
async function createOctokit(config, tokenProvider) {
|
|
1112
|
-
return new Octokit({
|
|
1113
|
-
auth: await tokenProvider.getToken(),
|
|
1114
|
-
request: {
|
|
1115
|
-
agent: config.agent,
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
async function createGitHubService(props) {
|
|
1120
|
-
var _a;
|
|
1121
|
-
const tokenProvider = (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new GitHubTokenCliProvider();
|
|
1122
|
-
return new GitHubService({
|
|
1123
|
-
config: props.config,
|
|
1124
|
-
octokit: await createOctokit(props.config, tokenProvider),
|
|
1125
|
-
cache: props.cache,
|
|
1126
|
-
tokenProvider,
|
|
1127
|
-
});
|
|
1079
|
+
}`;
|
|
1080
|
+
const issues = await this.cache.json(`renovate-bot-issues-${owner}-${repo}`, async () => {
|
|
1081
|
+
var _a, _b, _c, _d, _e;
|
|
1082
|
+
const result = [];
|
|
1083
|
+
let after = null;
|
|
1084
|
+
while (true) {
|
|
1085
|
+
const query = getQuery(after);
|
|
1086
|
+
const res = await this.runGraphqlQuery(query);
|
|
1087
|
+
const nodes = (_c = (_b = (_a = res.repository) === null || _a === void 0 ? void 0 : _a.issues.edges) === null || _b === void 0 ? void 0 : _b.map((it) => it.node)) !== null && _c !== void 0 ? _c : [];
|
|
1088
|
+
result.push(...nodes
|
|
1089
|
+
.filter((it) => it.title === "Dependency Dashboard")
|
|
1090
|
+
.map((it) => {
|
|
1091
|
+
var _a, _b, _c, _d, _e;
|
|
1092
|
+
return ({
|
|
1093
|
+
number: it.number,
|
|
1094
|
+
body: it.body,
|
|
1095
|
+
lastUpdatedByRenovate: (_e = (_d = (_c = (_b = (_a = it.userContentEdits) === null || _a === void 0 ? void 0 : _a.nodes) === null || _b === void 0 ? void 0 : _b.filter((it) => { var _a; return ((_a = it.editor) === null || _a === void 0 ? void 0 : _a.login) === "renovate"; })) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.createdAt) !== null && _e !== void 0 ? _e : null,
|
|
1096
|
+
});
|
|
1097
|
+
}));
|
|
1098
|
+
if (!((_d = res.repository) === null || _d === void 0 ? void 0 : _d.issues.pageInfo.hasNextPage)) {
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
after = (_e = res.repository) === null || _e === void 0 ? void 0 : _e.issues.pageInfo.endCursor;
|
|
1102
|
+
}
|
|
1103
|
+
return result;
|
|
1104
|
+
});
|
|
1105
|
+
if (issues.length == 0) {
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
1108
|
+
return issues[0];
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function createOctokit(config, tokenProvider) {
|
|
1112
|
+
return new Octokit({
|
|
1113
|
+
auth: await tokenProvider.getToken(),
|
|
1114
|
+
request: {
|
|
1115
|
+
agent: config.agent,
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async function createGitHubService(props) {
|
|
1120
|
+
var _a;
|
|
1121
|
+
const tokenProvider = (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new GitHubTokenCliProvider();
|
|
1122
|
+
return new GitHubService({
|
|
1123
|
+
config: props.config,
|
|
1124
|
+
octokit: await createOctokit(props.config, tokenProvider),
|
|
1125
|
+
cache: props.cache,
|
|
1126
|
+
tokenProvider,
|
|
1127
|
+
});
|
|
1128
1128
|
}
|
|
1129
1129
|
|
|
1130
1130
|
var index$4 = /*#__PURE__*/Object.freeze({
|
|
@@ -1142,275 +1142,275 @@ var index$3 = /*#__PURE__*/Object.freeze({
|
|
|
1142
1142
|
GitHubService: GitHubService
|
|
1143
1143
|
});
|
|
1144
1144
|
|
|
1145
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
1146
|
-
class LoadSecrets {
|
|
1147
|
-
constructor(props) {
|
|
1148
|
-
this.smClientForRegions = {};
|
|
1149
|
-
this.stsClient = new STSClient({
|
|
1150
|
-
region: "eu-west-1",
|
|
1151
|
-
});
|
|
1152
|
-
this.reporter = props.reporter;
|
|
1153
|
-
this.silent = props.silent;
|
|
1154
|
-
}
|
|
1155
|
-
getSmClient(region) {
|
|
1156
|
-
if (!this.smClientForRegions[region]) {
|
|
1157
|
-
this.smClientForRegions[region] = new SecretsManagerClient({
|
|
1158
|
-
region,
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
return this.smClientForRegions[region];
|
|
1162
|
-
}
|
|
1163
|
-
async getInput(options) {
|
|
1164
|
-
return new Promise((resolve, reject) => {
|
|
1165
|
-
read(options, (err, answer) => {
|
|
1166
|
-
if (err) {
|
|
1167
|
-
reject(err);
|
|
1168
|
-
}
|
|
1169
|
-
resolve(answer);
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
async getSecretDetails(client, secretId) {
|
|
1174
|
-
try {
|
|
1175
|
-
return await client.send(new DescribeSecretCommand({ SecretId: secretId }));
|
|
1176
|
-
}
|
|
1177
|
-
catch (e) {
|
|
1178
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1179
|
-
if (e.name && e.name === "ResourceNotFoundException") {
|
|
1180
|
-
return null;
|
|
1181
|
-
}
|
|
1182
|
-
throw e;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
async handleJsonUpdate(secret) {
|
|
1186
|
-
this.reporter.log("The secret is of type JSON with these expected fields:");
|
|
1187
|
-
for (const field of secret.fields) {
|
|
1188
|
-
const key = typeof field === "string" ? field : field.key;
|
|
1189
|
-
const desc = typeof field === "string"
|
|
1190
|
-
? ""
|
|
1191
|
-
: field.description
|
|
1192
|
-
? ` (${field.description})`
|
|
1193
|
-
: "";
|
|
1194
|
-
this.reporter.log(` - ${key}${desc}`);
|
|
1195
|
-
}
|
|
1196
|
-
this.reporter.log("");
|
|
1197
|
-
// TODO: Ability to specify full json value as one line.
|
|
1198
|
-
const collectedValues = {};
|
|
1199
|
-
for (const field of secret.fields) {
|
|
1200
|
-
const key = typeof field === "string" ? field : field.key;
|
|
1201
|
-
this.reporter.log(`Field: ${this.reporter.format.greenBright(key)}`);
|
|
1202
|
-
if (typeof field !== "string" && field.example != null) {
|
|
1203
|
-
this.reporter.log(`Example: ${this.reporter.format.magentaBright(field.example)}`);
|
|
1204
|
-
}
|
|
1205
|
-
const value = await this.getInput({
|
|
1206
|
-
prompt: "Enter value (Ctrl+C to abort): ",
|
|
1207
|
-
silent: this.silent,
|
|
1208
|
-
});
|
|
1209
|
-
collectedValues[key] = value;
|
|
1210
|
-
this.reporter.log("");
|
|
1211
|
-
}
|
|
1212
|
-
return JSON.stringify(collectedValues, undefined, " ");
|
|
1213
|
-
}
|
|
1214
|
-
getFullName(secretGroup, secret) {
|
|
1215
|
-
return `${secretGroup.namePrefix}${secret.name}`;
|
|
1216
|
-
}
|
|
1217
|
-
async syncTags(client, secret, tags) {
|
|
1218
|
-
const keysToRemove = secret
|
|
1219
|
-
.Tags.filter((existingTag) => !tags.some((it) => it.Key === existingTag.Key))
|
|
1220
|
-
.map((it) => it.Key);
|
|
1221
|
-
if (keysToRemove.length > 0) {
|
|
1222
|
-
this.reporter.log(`Removing obsolete tags: ${keysToRemove.join(", ")}`);
|
|
1223
|
-
await client.send(new UntagResourceCommand({
|
|
1224
|
-
SecretId: secret.ARN,
|
|
1225
|
-
TagKeys: keysToRemove,
|
|
1226
|
-
}));
|
|
1227
|
-
}
|
|
1228
|
-
const tagsToUpdate = tags.filter((expectedTag) => {
|
|
1229
|
-
const existing = secret.Tags.find((it) => it.Key === expectedTag.Key);
|
|
1230
|
-
return existing == null || existing.Value != expectedTag.Value;
|
|
1231
|
-
});
|
|
1232
|
-
if (tagsToUpdate.length > 0) {
|
|
1233
|
-
this.reporter.log(`Storing tags: ${tagsToUpdate.map((it) => it.Key).join(", ")}`);
|
|
1234
|
-
await client.send(new TagResourceCommand({
|
|
1235
|
-
SecretId: secret.ARN,
|
|
1236
|
-
Tags: tagsToUpdate,
|
|
1237
|
-
}));
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
async getSecretValue(client, secretId) {
|
|
1241
|
-
const result = await client.send(new GetSecretValueCommand({
|
|
1242
|
-
SecretId: secretId,
|
|
1243
|
-
}));
|
|
1244
|
-
if (result.SecretString == null) {
|
|
1245
|
-
throw new Error("Missing SecretString (is it a binary?)");
|
|
1246
|
-
}
|
|
1247
|
-
return result.SecretString;
|
|
1248
|
-
}
|
|
1249
|
-
async handleUpdate(secretGroup, secret) {
|
|
1250
|
-
const client = this.getSmClient(secretGroup.region);
|
|
1251
|
-
const fullName = this.getFullName(secretGroup, secret);
|
|
1252
|
-
const describeSecret = await this.getSecretDetails(client, fullName);
|
|
1253
|
-
this.reporter.log(`Secret: ${this.reporter.format.greenBright(fullName)}`);
|
|
1254
|
-
if (describeSecret == null) {
|
|
1255
|
-
this.reporter.log("The secret does not already exist and will be created");
|
|
1256
|
-
}
|
|
1257
|
-
else {
|
|
1258
|
-
this.reporter.log("Current value:");
|
|
1259
|
-
this.reporter.log(this.reporter.format.yellowBright((await this.getSecretValue(client, fullName)).replace(/^/gm, " ")));
|
|
1260
|
-
}
|
|
1261
|
-
this.reporter.log("");
|
|
1262
|
-
let secretValue;
|
|
1263
|
-
if (secret.type === "json") {
|
|
1264
|
-
try {
|
|
1265
|
-
secretValue = await this.handleJsonUpdate(secret);
|
|
1266
|
-
}
|
|
1267
|
-
catch (e) {
|
|
1268
|
-
if (e instanceof Error && e.message === "canceled") {
|
|
1269
|
-
this.reporter.log("Aborted");
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
throw e;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
else {
|
|
1276
|
-
throw new Error(`Unsupported type`);
|
|
1277
|
-
}
|
|
1278
|
-
this.reporter.log("Storing secret value:");
|
|
1279
|
-
this.reporter.log(this.reporter.format.yellowBright(secretValue.replace(/^/gm, " ")));
|
|
1280
|
-
const tags = [
|
|
1281
|
-
{
|
|
1282
|
-
Key: "Source",
|
|
1283
|
-
Value: "load-secrets script",
|
|
1284
|
-
},
|
|
1285
|
-
];
|
|
1286
|
-
let arn;
|
|
1287
|
-
let version;
|
|
1288
|
-
if (describeSecret == null) {
|
|
1289
|
-
const createResult = await client.send(new CreateSecretCommand({
|
|
1290
|
-
Name: fullName,
|
|
1291
|
-
Description: "Created by load-secrets",
|
|
1292
|
-
SecretString: secretValue,
|
|
1293
|
-
Tags: tags,
|
|
1294
|
-
}));
|
|
1295
|
-
if (createResult.VersionId == null) {
|
|
1296
|
-
throw new Error("Expected versionId");
|
|
1297
|
-
}
|
|
1298
|
-
arn = createResult.ARN;
|
|
1299
|
-
version = createResult.VersionId;
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
if (describeSecret.DeletedDate != null) {
|
|
1303
|
-
await client.send(new RestoreSecretCommand({
|
|
1304
|
-
SecretId: fullName,
|
|
1305
|
-
}));
|
|
1306
|
-
}
|
|
1307
|
-
const updateResult = await client.send(new PutSecretValueCommand({
|
|
1308
|
-
SecretId: fullName,
|
|
1309
|
-
SecretString: secretValue,
|
|
1310
|
-
}));
|
|
1311
|
-
if (updateResult.VersionId == null) {
|
|
1312
|
-
throw new Error("Expected versionId");
|
|
1313
|
-
}
|
|
1314
|
-
await this.syncTags(client, describeSecret, tags);
|
|
1315
|
-
arn = updateResult.ARN;
|
|
1316
|
-
version = updateResult.VersionId;
|
|
1317
|
-
}
|
|
1318
|
-
this.reporter.log("");
|
|
1319
|
-
this.reporter.log("Secret stored:");
|
|
1320
|
-
this.reporter.log(`ARN: ${this.reporter.format.greenBright(arn)}`);
|
|
1321
|
-
this.reporter.log(`Version: ${this.reporter.format.greenBright(version)}`);
|
|
1322
|
-
}
|
|
1323
|
-
checkSecretGroup(secretGroup) {
|
|
1324
|
-
if (!secretGroup.namePrefix.startsWith("/") ||
|
|
1325
|
-
!secretGroup.namePrefix.endsWith("/")) {
|
|
1326
|
-
throw new Error(`namePrefix should start and end with /. Current value: ${secretGroup.namePrefix}`);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
getSecretDescription(details) {
|
|
1330
|
-
var _a, _b;
|
|
1331
|
-
return details == null
|
|
1332
|
-
? "not yet created"
|
|
1333
|
-
: (details === null || details === void 0 ? void 0 : details.DeletedDate) != null
|
|
1334
|
-
? `scheduled for deletion ${details.DeletedDate.toISOString()}`
|
|
1335
|
-
: `last changed ${(_b = (_a = details.LastChangedDate) === null || _a === void 0 ? void 0 : _a.toISOString()) !== null && _b !== void 0 ? _b : "unknown"}`;
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* Returns false if aborted.
|
|
1339
|
-
*/
|
|
1340
|
-
async selectAndUpdate(secretGroups) {
|
|
1341
|
-
const secrets = [];
|
|
1342
|
-
this.reporter.log("Select secret to write:");
|
|
1343
|
-
this.reporter.log("");
|
|
1344
|
-
for (const secretGroup of secretGroups) {
|
|
1345
|
-
this.reporter.log(`${secretGroup.description} (prefix: ${secretGroup.namePrefix})`);
|
|
1346
|
-
for (let i = 0; i < secretGroup.secrets.length; i++) {
|
|
1347
|
-
const offset = secrets.length;
|
|
1348
|
-
const secret = secretGroup.secrets[i];
|
|
1349
|
-
secrets.push({
|
|
1350
|
-
secret: secret,
|
|
1351
|
-
secretGroup,
|
|
1352
|
-
});
|
|
1353
|
-
const client = this.getSmClient(secretGroup.region);
|
|
1354
|
-
const details = await this.getSecretDetails(client, this.getFullName(secretGroup, secret));
|
|
1355
|
-
const desc = this.getSecretDescription(details);
|
|
1356
|
-
this.reporter.log(` (${offset}) ${secret.name} (${desc})`);
|
|
1357
|
-
}
|
|
1358
|
-
this.reporter.log("");
|
|
1359
|
-
}
|
|
1360
|
-
let index;
|
|
1361
|
-
try {
|
|
1362
|
-
const answer = await this.getInput({
|
|
1363
|
-
prompt: "Enter index (or enter to quit): ",
|
|
1364
|
-
});
|
|
1365
|
-
if (answer.trim() === "") {
|
|
1366
|
-
return false;
|
|
1367
|
-
}
|
|
1368
|
-
index = parseInt(answer);
|
|
1369
|
-
if (!secrets[index]) {
|
|
1370
|
-
throw new Error();
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
catch (e) {
|
|
1374
|
-
this.reporter.warn("Secret not found - aborting");
|
|
1375
|
-
return false;
|
|
1376
|
-
}
|
|
1377
|
-
this.reporter.log("");
|
|
1378
|
-
await this.handleUpdate(secrets[index].secretGroup, secrets[index].secret);
|
|
1379
|
-
this.reporter.log("");
|
|
1380
|
-
return true;
|
|
1381
|
-
}
|
|
1382
|
-
async process(secretGroups) {
|
|
1383
|
-
this.reporter.info("Checking account for current credentials");
|
|
1384
|
-
this.reporter.info("If any error is given, make sure you have valid credentials active");
|
|
1385
|
-
const currentAccount = await this.stsClient.send(new GetCallerIdentityCommand({}));
|
|
1386
|
-
this.reporter.info(`Running for account ${currentAccount.Account}`);
|
|
1387
|
-
this.reporter.log("");
|
|
1388
|
-
const matchedSecretGroups = secretGroups.filter((it) => it.accountId === currentAccount.Account);
|
|
1389
|
-
if (matchedSecretGroups.length === 0) {
|
|
1390
|
-
this.reporter.error(`No secrets specified for this account - aborting`);
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
for (const secretGroup of matchedSecretGroups) {
|
|
1394
|
-
this.checkSecretGroup(secretGroup);
|
|
1395
|
-
}
|
|
1396
|
-
while (await this.selectAndUpdate(matchedSecretGroups)) { }
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Load secrets interactively into Secrets Manager.
|
|
1401
|
-
*/
|
|
1402
|
-
function loadSecretsCli(props) {
|
|
1403
|
-
const loadSecrets = new LoadSecrets({
|
|
1404
|
-
reporter: createReporter({}),
|
|
1405
|
-
// For now we show the secrets, so that we get positive feedback that the value
|
|
1406
|
-
// is correctly entered.
|
|
1407
|
-
silent: false,
|
|
1408
|
-
});
|
|
1409
|
-
loadSecrets.process(props.secretGroups).catch((error) => {
|
|
1410
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1411
|
-
console.error(error.stack || error.message || error);
|
|
1412
|
-
process.exitCode = 1;
|
|
1413
|
-
});
|
|
1145
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
1146
|
+
class LoadSecrets {
|
|
1147
|
+
constructor(props) {
|
|
1148
|
+
this.smClientForRegions = {};
|
|
1149
|
+
this.stsClient = new STSClient({
|
|
1150
|
+
region: "eu-west-1",
|
|
1151
|
+
});
|
|
1152
|
+
this.reporter = props.reporter;
|
|
1153
|
+
this.silent = props.silent;
|
|
1154
|
+
}
|
|
1155
|
+
getSmClient(region) {
|
|
1156
|
+
if (!this.smClientForRegions[region]) {
|
|
1157
|
+
this.smClientForRegions[region] = new SecretsManagerClient({
|
|
1158
|
+
region,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
return this.smClientForRegions[region];
|
|
1162
|
+
}
|
|
1163
|
+
async getInput(options) {
|
|
1164
|
+
return new Promise((resolve, reject) => {
|
|
1165
|
+
read(options, (err, answer) => {
|
|
1166
|
+
if (err) {
|
|
1167
|
+
reject(err);
|
|
1168
|
+
}
|
|
1169
|
+
resolve(answer);
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
async getSecretDetails(client, secretId) {
|
|
1174
|
+
try {
|
|
1175
|
+
return await client.send(new DescribeSecretCommand({ SecretId: secretId }));
|
|
1176
|
+
}
|
|
1177
|
+
catch (e) {
|
|
1178
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1179
|
+
if (e.name && e.name === "ResourceNotFoundException") {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
throw e;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async handleJsonUpdate(secret) {
|
|
1186
|
+
this.reporter.log("The secret is of type JSON with these expected fields:");
|
|
1187
|
+
for (const field of secret.fields) {
|
|
1188
|
+
const key = typeof field === "string" ? field : field.key;
|
|
1189
|
+
const desc = typeof field === "string"
|
|
1190
|
+
? ""
|
|
1191
|
+
: field.description
|
|
1192
|
+
? ` (${field.description})`
|
|
1193
|
+
: "";
|
|
1194
|
+
this.reporter.log(` - ${key}${desc}`);
|
|
1195
|
+
}
|
|
1196
|
+
this.reporter.log("");
|
|
1197
|
+
// TODO: Ability to specify full json value as one line.
|
|
1198
|
+
const collectedValues = {};
|
|
1199
|
+
for (const field of secret.fields) {
|
|
1200
|
+
const key = typeof field === "string" ? field : field.key;
|
|
1201
|
+
this.reporter.log(`Field: ${this.reporter.format.greenBright(key)}`);
|
|
1202
|
+
if (typeof field !== "string" && field.example != null) {
|
|
1203
|
+
this.reporter.log(`Example: ${this.reporter.format.magentaBright(field.example)}`);
|
|
1204
|
+
}
|
|
1205
|
+
const value = await this.getInput({
|
|
1206
|
+
prompt: "Enter value (Ctrl+C to abort): ",
|
|
1207
|
+
silent: this.silent,
|
|
1208
|
+
});
|
|
1209
|
+
collectedValues[key] = value;
|
|
1210
|
+
this.reporter.log("");
|
|
1211
|
+
}
|
|
1212
|
+
return JSON.stringify(collectedValues, undefined, " ");
|
|
1213
|
+
}
|
|
1214
|
+
getFullName(secretGroup, secret) {
|
|
1215
|
+
return `${secretGroup.namePrefix}${secret.name}`;
|
|
1216
|
+
}
|
|
1217
|
+
async syncTags(client, secret, tags) {
|
|
1218
|
+
const keysToRemove = secret
|
|
1219
|
+
.Tags.filter((existingTag) => !tags.some((it) => it.Key === existingTag.Key))
|
|
1220
|
+
.map((it) => it.Key);
|
|
1221
|
+
if (keysToRemove.length > 0) {
|
|
1222
|
+
this.reporter.log(`Removing obsolete tags: ${keysToRemove.join(", ")}`);
|
|
1223
|
+
await client.send(new UntagResourceCommand({
|
|
1224
|
+
SecretId: secret.ARN,
|
|
1225
|
+
TagKeys: keysToRemove,
|
|
1226
|
+
}));
|
|
1227
|
+
}
|
|
1228
|
+
const tagsToUpdate = tags.filter((expectedTag) => {
|
|
1229
|
+
const existing = secret.Tags.find((it) => it.Key === expectedTag.Key);
|
|
1230
|
+
return existing == null || existing.Value != expectedTag.Value;
|
|
1231
|
+
});
|
|
1232
|
+
if (tagsToUpdate.length > 0) {
|
|
1233
|
+
this.reporter.log(`Storing tags: ${tagsToUpdate.map((it) => it.Key).join(", ")}`);
|
|
1234
|
+
await client.send(new TagResourceCommand({
|
|
1235
|
+
SecretId: secret.ARN,
|
|
1236
|
+
Tags: tagsToUpdate,
|
|
1237
|
+
}));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async getSecretValue(client, secretId) {
|
|
1241
|
+
const result = await client.send(new GetSecretValueCommand({
|
|
1242
|
+
SecretId: secretId,
|
|
1243
|
+
}));
|
|
1244
|
+
if (result.SecretString == null) {
|
|
1245
|
+
throw new Error("Missing SecretString (is it a binary?)");
|
|
1246
|
+
}
|
|
1247
|
+
return result.SecretString;
|
|
1248
|
+
}
|
|
1249
|
+
async handleUpdate(secretGroup, secret) {
|
|
1250
|
+
const client = this.getSmClient(secretGroup.region);
|
|
1251
|
+
const fullName = this.getFullName(secretGroup, secret);
|
|
1252
|
+
const describeSecret = await this.getSecretDetails(client, fullName);
|
|
1253
|
+
this.reporter.log(`Secret: ${this.reporter.format.greenBright(fullName)}`);
|
|
1254
|
+
if (describeSecret == null) {
|
|
1255
|
+
this.reporter.log("The secret does not already exist and will be created");
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
this.reporter.log("Current value:");
|
|
1259
|
+
this.reporter.log(this.reporter.format.yellowBright((await this.getSecretValue(client, fullName)).replace(/^/gm, " ")));
|
|
1260
|
+
}
|
|
1261
|
+
this.reporter.log("");
|
|
1262
|
+
let secretValue;
|
|
1263
|
+
if (secret.type === "json") {
|
|
1264
|
+
try {
|
|
1265
|
+
secretValue = await this.handleJsonUpdate(secret);
|
|
1266
|
+
}
|
|
1267
|
+
catch (e) {
|
|
1268
|
+
if (e instanceof Error && e.message === "canceled") {
|
|
1269
|
+
this.reporter.log("Aborted");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
throw e;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
throw new Error(`Unsupported type`);
|
|
1277
|
+
}
|
|
1278
|
+
this.reporter.log("Storing secret value:");
|
|
1279
|
+
this.reporter.log(this.reporter.format.yellowBright(secretValue.replace(/^/gm, " ")));
|
|
1280
|
+
const tags = [
|
|
1281
|
+
{
|
|
1282
|
+
Key: "Source",
|
|
1283
|
+
Value: "load-secrets script",
|
|
1284
|
+
},
|
|
1285
|
+
];
|
|
1286
|
+
let arn;
|
|
1287
|
+
let version;
|
|
1288
|
+
if (describeSecret == null) {
|
|
1289
|
+
const createResult = await client.send(new CreateSecretCommand({
|
|
1290
|
+
Name: fullName,
|
|
1291
|
+
Description: "Created by load-secrets",
|
|
1292
|
+
SecretString: secretValue,
|
|
1293
|
+
Tags: tags,
|
|
1294
|
+
}));
|
|
1295
|
+
if (createResult.VersionId == null) {
|
|
1296
|
+
throw new Error("Expected versionId");
|
|
1297
|
+
}
|
|
1298
|
+
arn = createResult.ARN;
|
|
1299
|
+
version = createResult.VersionId;
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
if (describeSecret.DeletedDate != null) {
|
|
1303
|
+
await client.send(new RestoreSecretCommand({
|
|
1304
|
+
SecretId: fullName,
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
const updateResult = await client.send(new PutSecretValueCommand({
|
|
1308
|
+
SecretId: fullName,
|
|
1309
|
+
SecretString: secretValue,
|
|
1310
|
+
}));
|
|
1311
|
+
if (updateResult.VersionId == null) {
|
|
1312
|
+
throw new Error("Expected versionId");
|
|
1313
|
+
}
|
|
1314
|
+
await this.syncTags(client, describeSecret, tags);
|
|
1315
|
+
arn = updateResult.ARN;
|
|
1316
|
+
version = updateResult.VersionId;
|
|
1317
|
+
}
|
|
1318
|
+
this.reporter.log("");
|
|
1319
|
+
this.reporter.log("Secret stored:");
|
|
1320
|
+
this.reporter.log(`ARN: ${this.reporter.format.greenBright(arn)}`);
|
|
1321
|
+
this.reporter.log(`Version: ${this.reporter.format.greenBright(version)}`);
|
|
1322
|
+
}
|
|
1323
|
+
checkSecretGroup(secretGroup) {
|
|
1324
|
+
if (!secretGroup.namePrefix.startsWith("/") ||
|
|
1325
|
+
!secretGroup.namePrefix.endsWith("/")) {
|
|
1326
|
+
throw new Error(`namePrefix should start and end with /. Current value: ${secretGroup.namePrefix}`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
getSecretDescription(details) {
|
|
1330
|
+
var _a, _b;
|
|
1331
|
+
return details == null
|
|
1332
|
+
? "not yet created"
|
|
1333
|
+
: (details === null || details === void 0 ? void 0 : details.DeletedDate) != null
|
|
1334
|
+
? `scheduled for deletion ${details.DeletedDate.toISOString()}`
|
|
1335
|
+
: `last changed ${(_b = (_a = details.LastChangedDate) === null || _a === void 0 ? void 0 : _a.toISOString()) !== null && _b !== void 0 ? _b : "unknown"}`;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Returns false if aborted.
|
|
1339
|
+
*/
|
|
1340
|
+
async selectAndUpdate(secretGroups) {
|
|
1341
|
+
const secrets = [];
|
|
1342
|
+
this.reporter.log("Select secret to write:");
|
|
1343
|
+
this.reporter.log("");
|
|
1344
|
+
for (const secretGroup of secretGroups) {
|
|
1345
|
+
this.reporter.log(`${secretGroup.description} (prefix: ${secretGroup.namePrefix})`);
|
|
1346
|
+
for (let i = 0; i < secretGroup.secrets.length; i++) {
|
|
1347
|
+
const offset = secrets.length;
|
|
1348
|
+
const secret = secretGroup.secrets[i];
|
|
1349
|
+
secrets.push({
|
|
1350
|
+
secret: secret,
|
|
1351
|
+
secretGroup,
|
|
1352
|
+
});
|
|
1353
|
+
const client = this.getSmClient(secretGroup.region);
|
|
1354
|
+
const details = await this.getSecretDetails(client, this.getFullName(secretGroup, secret));
|
|
1355
|
+
const desc = this.getSecretDescription(details);
|
|
1356
|
+
this.reporter.log(` (${offset}) ${secret.name} (${desc})`);
|
|
1357
|
+
}
|
|
1358
|
+
this.reporter.log("");
|
|
1359
|
+
}
|
|
1360
|
+
let index;
|
|
1361
|
+
try {
|
|
1362
|
+
const answer = await this.getInput({
|
|
1363
|
+
prompt: "Enter index (or enter to quit): ",
|
|
1364
|
+
});
|
|
1365
|
+
if (answer.trim() === "") {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
index = parseInt(answer);
|
|
1369
|
+
if (!secrets[index]) {
|
|
1370
|
+
throw new Error();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
catch (e) {
|
|
1374
|
+
this.reporter.warn("Secret not found - aborting");
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
this.reporter.log("");
|
|
1378
|
+
await this.handleUpdate(secrets[index].secretGroup, secrets[index].secret);
|
|
1379
|
+
this.reporter.log("");
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
async process(secretGroups) {
|
|
1383
|
+
this.reporter.info("Checking account for current credentials");
|
|
1384
|
+
this.reporter.info("If any error is given, make sure you have valid credentials active");
|
|
1385
|
+
const currentAccount = await this.stsClient.send(new GetCallerIdentityCommand({}));
|
|
1386
|
+
this.reporter.info(`Running for account ${currentAccount.Account}`);
|
|
1387
|
+
this.reporter.log("");
|
|
1388
|
+
const matchedSecretGroups = secretGroups.filter((it) => it.accountId === currentAccount.Account);
|
|
1389
|
+
if (matchedSecretGroups.length === 0) {
|
|
1390
|
+
this.reporter.error(`No secrets specified for this account - aborting`);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
for (const secretGroup of matchedSecretGroups) {
|
|
1394
|
+
this.checkSecretGroup(secretGroup);
|
|
1395
|
+
}
|
|
1396
|
+
while (await this.selectAndUpdate(matchedSecretGroups)) { }
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Load secrets interactively into Secrets Manager.
|
|
1401
|
+
*/
|
|
1402
|
+
function loadSecretsCli(props) {
|
|
1403
|
+
const loadSecrets = new LoadSecrets({
|
|
1404
|
+
reporter: createReporter({}),
|
|
1405
|
+
// For now we show the secrets, so that we get positive feedback that the value
|
|
1406
|
+
// is correctly entered.
|
|
1407
|
+
silent: false,
|
|
1408
|
+
});
|
|
1409
|
+
loadSecrets.process(props.secretGroups).catch((error) => {
|
|
1410
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1411
|
+
console.error(error.stack || error.message || error);
|
|
1412
|
+
process.exitCode = 1;
|
|
1413
|
+
});
|
|
1414
1414
|
}
|
|
1415
1415
|
|
|
1416
1416
|
var index$2 = /*#__PURE__*/Object.freeze({
|
|
@@ -1418,105 +1418,105 @@ var index$2 = /*#__PURE__*/Object.freeze({
|
|
|
1418
1418
|
loadSecretsCli: loadSecretsCli
|
|
1419
1419
|
});
|
|
1420
1420
|
|
|
1421
|
-
class SnykTokenCliProvider {
|
|
1422
|
-
constructor() {
|
|
1423
|
-
this.keyringService = "cals";
|
|
1424
|
-
this.keyringAccount = "snyk-token";
|
|
1425
|
-
}
|
|
1426
|
-
async getToken() {
|
|
1427
|
-
if (process.env.CALS_SNYK_TOKEN) {
|
|
1428
|
-
return process.env.CALS_SNYK_TOKEN;
|
|
1429
|
-
}
|
|
1430
|
-
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
1431
|
-
if (result == null) {
|
|
1432
|
-
process.stderr.write("No token found. Register using `cals snyk set-token`\n");
|
|
1433
|
-
return undefined;
|
|
1434
|
-
}
|
|
1435
|
-
return result;
|
|
1436
|
-
}
|
|
1437
|
-
async markInvalid() {
|
|
1438
|
-
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
1439
|
-
}
|
|
1440
|
-
async setToken(value) {
|
|
1441
|
-
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
1442
|
-
}
|
|
1421
|
+
class SnykTokenCliProvider {
|
|
1422
|
+
constructor() {
|
|
1423
|
+
this.keyringService = "cals";
|
|
1424
|
+
this.keyringAccount = "snyk-token";
|
|
1425
|
+
}
|
|
1426
|
+
async getToken() {
|
|
1427
|
+
if (process.env.CALS_SNYK_TOKEN) {
|
|
1428
|
+
return process.env.CALS_SNYK_TOKEN;
|
|
1429
|
+
}
|
|
1430
|
+
const result = await keytar.getPassword(this.keyringService, this.keyringAccount);
|
|
1431
|
+
if (result == null) {
|
|
1432
|
+
process.stderr.write("No token found. Register using `cals snyk set-token`\n");
|
|
1433
|
+
return undefined;
|
|
1434
|
+
}
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1437
|
+
async markInvalid() {
|
|
1438
|
+
await keytar.deletePassword(this.keyringService, this.keyringAccount);
|
|
1439
|
+
}
|
|
1440
|
+
async setToken(value) {
|
|
1441
|
+
await keytar.setPassword(this.keyringService, this.keyringAccount, value);
|
|
1442
|
+
}
|
|
1443
1443
|
}
|
|
1444
1444
|
|
|
1445
|
-
class SnykService {
|
|
1446
|
-
constructor(props) {
|
|
1447
|
-
this.config = props.config;
|
|
1448
|
-
this.tokenProvider = props.tokenProvider;
|
|
1449
|
-
}
|
|
1450
|
-
async getProjects(definition) {
|
|
1451
|
-
var _a;
|
|
1452
|
-
const snykAccountId = (_a = definition.snyk) === null || _a === void 0 ? void 0 : _a.accountId;
|
|
1453
|
-
if (snykAccountId === undefined) {
|
|
1454
|
-
return [];
|
|
1455
|
-
}
|
|
1456
|
-
return this.getProjectsByAccountId(snykAccountId);
|
|
1457
|
-
}
|
|
1458
|
-
async getProjectsByAccountId(snykAccountId) {
|
|
1459
|
-
const token = await this.tokenProvider.getToken();
|
|
1460
|
-
if (token === undefined) {
|
|
1461
|
-
throw new Error("Missing token for Snyk");
|
|
1462
|
-
}
|
|
1463
|
-
const response = await fetch(`https://snyk.io/api/v1/org/${encodeURIComponent(snykAccountId)}/projects`, {
|
|
1464
|
-
method: "GET",
|
|
1465
|
-
headers: {
|
|
1466
|
-
Accept: "application/json",
|
|
1467
|
-
Authorization: `token ${token}`,
|
|
1468
|
-
},
|
|
1469
|
-
agent: this.config.agent,
|
|
1470
|
-
});
|
|
1471
|
-
if (response.status === 401) {
|
|
1472
|
-
process.stderr.write("Unauthorized - removing token\n");
|
|
1473
|
-
await this.tokenProvider.markInvalid();
|
|
1474
|
-
}
|
|
1475
|
-
if (!response.ok) {
|
|
1476
|
-
throw new Error(`Response from Snyk not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
1477
|
-
}
|
|
1478
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1479
|
-
return (await response.json()).projects;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
function createSnykService(props) {
|
|
1483
|
-
var _a;
|
|
1484
|
-
return new SnykService({
|
|
1485
|
-
config: props.config,
|
|
1486
|
-
tokenProvider: (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new SnykTokenCliProvider(),
|
|
1487
|
-
});
|
|
1445
|
+
class SnykService {
|
|
1446
|
+
constructor(props) {
|
|
1447
|
+
this.config = props.config;
|
|
1448
|
+
this.tokenProvider = props.tokenProvider;
|
|
1449
|
+
}
|
|
1450
|
+
async getProjects(definition) {
|
|
1451
|
+
var _a;
|
|
1452
|
+
const snykAccountId = (_a = definition.snyk) === null || _a === void 0 ? void 0 : _a.accountId;
|
|
1453
|
+
if (snykAccountId === undefined) {
|
|
1454
|
+
return [];
|
|
1455
|
+
}
|
|
1456
|
+
return this.getProjectsByAccountId(snykAccountId);
|
|
1457
|
+
}
|
|
1458
|
+
async getProjectsByAccountId(snykAccountId) {
|
|
1459
|
+
const token = await this.tokenProvider.getToken();
|
|
1460
|
+
if (token === undefined) {
|
|
1461
|
+
throw new Error("Missing token for Snyk");
|
|
1462
|
+
}
|
|
1463
|
+
const response = await fetch(`https://snyk.io/api/v1/org/${encodeURIComponent(snykAccountId)}/projects`, {
|
|
1464
|
+
method: "GET",
|
|
1465
|
+
headers: {
|
|
1466
|
+
Accept: "application/json",
|
|
1467
|
+
Authorization: `token ${token}`,
|
|
1468
|
+
},
|
|
1469
|
+
agent: this.config.agent,
|
|
1470
|
+
});
|
|
1471
|
+
if (response.status === 401) {
|
|
1472
|
+
process.stderr.write("Unauthorized - removing token\n");
|
|
1473
|
+
await this.tokenProvider.markInvalid();
|
|
1474
|
+
}
|
|
1475
|
+
if (!response.ok) {
|
|
1476
|
+
throw new Error(`Response from Snyk not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
1477
|
+
}
|
|
1478
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1479
|
+
return (await response.json()).projects;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function createSnykService(props) {
|
|
1483
|
+
var _a;
|
|
1484
|
+
return new SnykService({
|
|
1485
|
+
config: props.config,
|
|
1486
|
+
tokenProvider: (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new SnykTokenCliProvider(),
|
|
1487
|
+
});
|
|
1488
1488
|
}
|
|
1489
1489
|
|
|
1490
|
-
function getGitHubRepo(snykProject) {
|
|
1491
|
-
if (snykProject.origin === "github") {
|
|
1492
|
-
const match = /^([^/]+)\/([^:]+)(:(.+))?$/.exec(snykProject.name);
|
|
1493
|
-
if (match === null) {
|
|
1494
|
-
throw Error(`Could not extract components from Snyk project name: ${snykProject.name} (id: ${snykProject.id})`);
|
|
1495
|
-
}
|
|
1496
|
-
return {
|
|
1497
|
-
owner: match[1],
|
|
1498
|
-
name: match[2],
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
else if (snykProject.origin === "cli" &&
|
|
1502
|
-
snykProject.remoteRepoUrl != null) {
|
|
1503
|
-
// The remoteRepoUrl can be overriden when using the CLI, so don't
|
|
1504
|
-
// fail if we cannot extract the value.
|
|
1505
|
-
const match = /github.com\/([^/]+)\/(.+)\.git$/.exec(snykProject.remoteRepoUrl);
|
|
1506
|
-
if (match === null) {
|
|
1507
|
-
return undefined;
|
|
1508
|
-
}
|
|
1509
|
-
return {
|
|
1510
|
-
owner: match[1],
|
|
1511
|
-
name: match[2],
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
else {
|
|
1515
|
-
return undefined;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
function getGitHubRepoId(repo) {
|
|
1519
|
-
return repo ? `${repo.owner}/${repo.name}` : undefined;
|
|
1490
|
+
function getGitHubRepo(snykProject) {
|
|
1491
|
+
if (snykProject.origin === "github") {
|
|
1492
|
+
const match = /^([^/]+)\/([^:]+)(:(.+))?$/.exec(snykProject.name);
|
|
1493
|
+
if (match === null) {
|
|
1494
|
+
throw Error(`Could not extract components from Snyk project name: ${snykProject.name} (id: ${snykProject.id})`);
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
owner: match[1],
|
|
1498
|
+
name: match[2],
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
else if (snykProject.origin === "cli" &&
|
|
1502
|
+
snykProject.remoteRepoUrl != null) {
|
|
1503
|
+
// The remoteRepoUrl can be overriden when using the CLI, so don't
|
|
1504
|
+
// fail if we cannot extract the value.
|
|
1505
|
+
const match = /github.com\/([^/]+)\/(.+)\.git$/.exec(snykProject.remoteRepoUrl);
|
|
1506
|
+
if (match === null) {
|
|
1507
|
+
return undefined;
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
owner: match[1],
|
|
1511
|
+
name: match[2],
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function getGitHubRepoId(repo) {
|
|
1519
|
+
return repo ? `${repo.owner}/${repo.name}` : undefined;
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
1522
|
var index$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -1527,61 +1527,61 @@ var index$1 = /*#__PURE__*/Object.freeze({
|
|
|
1527
1527
|
getGitHubRepoId: getGitHubRepoId
|
|
1528
1528
|
});
|
|
1529
1529
|
|
|
1530
|
-
class SonarCloudTokenCliProvider {
|
|
1531
|
-
async getToken() {
|
|
1532
|
-
if (process.env.CALS_SONARCLOUD_TOKEN) {
|
|
1533
|
-
return Promise.resolve(process.env.CALS_SONARCLOUD_TOKEN);
|
|
1534
|
-
}
|
|
1535
|
-
process.stderr.write("No environmental variable found. Set variable `CALS_SONARCLOUD_TOKEN` to token value\n");
|
|
1536
|
-
return undefined;
|
|
1537
|
-
}
|
|
1538
|
-
async markInvalid() {
|
|
1539
|
-
await Promise.resolve();
|
|
1540
|
-
}
|
|
1530
|
+
class SonarCloudTokenCliProvider {
|
|
1531
|
+
async getToken() {
|
|
1532
|
+
if (process.env.CALS_SONARCLOUD_TOKEN) {
|
|
1533
|
+
return Promise.resolve(process.env.CALS_SONARCLOUD_TOKEN);
|
|
1534
|
+
}
|
|
1535
|
+
process.stderr.write("No environmental variable found. Set variable `CALS_SONARCLOUD_TOKEN` to token value\n");
|
|
1536
|
+
return undefined;
|
|
1537
|
+
}
|
|
1538
|
+
async markInvalid() {
|
|
1539
|
+
await Promise.resolve();
|
|
1540
|
+
}
|
|
1541
1541
|
}
|
|
1542
1542
|
|
|
1543
|
-
class SonarCloudService {
|
|
1544
|
-
constructor(props) {
|
|
1545
|
-
this.config = props.config;
|
|
1546
|
-
this.tokenProvider = props.tokenProvider;
|
|
1547
|
-
}
|
|
1548
|
-
/**
|
|
1549
|
-
* Returns metrics for project with given key.
|
|
1550
|
-
* ONLY test coverage metrics are returned as of now
|
|
1551
|
-
*/
|
|
1552
|
-
async getMetricsByProjectKey(sonarCloudProjectKey) {
|
|
1553
|
-
const token = await this.tokenProvider.getToken();
|
|
1554
|
-
if (token === undefined) {
|
|
1555
|
-
throw new Error("Missing token for SonarCloud");
|
|
1556
|
-
}
|
|
1557
|
-
const response = await fetch(`https://sonarcloud.io/api/measures/component?component=${encodeURIComponent(sonarCloudProjectKey)}&metricKeys=coverage`, {
|
|
1558
|
-
method: "GET",
|
|
1559
|
-
headers: {
|
|
1560
|
-
Accept: "application/json",
|
|
1561
|
-
Authorization: `Basic ${Buffer.from(token.concat(":"), "utf8").toString("base64")}`,
|
|
1562
|
-
},
|
|
1563
|
-
agent: this.config.agent,
|
|
1564
|
-
});
|
|
1565
|
-
if (response.status === 401) {
|
|
1566
|
-
process.stderr.write("Unauthorized - removing token\n");
|
|
1567
|
-
await this.tokenProvider.markInvalid();
|
|
1568
|
-
}
|
|
1569
|
-
if (response.status === 404) {
|
|
1570
|
-
process.stderr.write("Project does not exist in SonarCloud\n");
|
|
1571
|
-
return undefined;
|
|
1572
|
-
}
|
|
1573
|
-
if (!response.ok) {
|
|
1574
|
-
throw new Error(`Response from SonarCloud not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
1575
|
-
}
|
|
1576
|
-
return (await response.json());
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
function createSonarCloudService(props) {
|
|
1580
|
-
var _a;
|
|
1581
|
-
return new SonarCloudService({
|
|
1582
|
-
config: props.config,
|
|
1583
|
-
tokenProvider: (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new SonarCloudTokenCliProvider(),
|
|
1584
|
-
});
|
|
1543
|
+
class SonarCloudService {
|
|
1544
|
+
constructor(props) {
|
|
1545
|
+
this.config = props.config;
|
|
1546
|
+
this.tokenProvider = props.tokenProvider;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Returns metrics for project with given key.
|
|
1550
|
+
* ONLY test coverage metrics are returned as of now
|
|
1551
|
+
*/
|
|
1552
|
+
async getMetricsByProjectKey(sonarCloudProjectKey) {
|
|
1553
|
+
const token = await this.tokenProvider.getToken();
|
|
1554
|
+
if (token === undefined) {
|
|
1555
|
+
throw new Error("Missing token for SonarCloud");
|
|
1556
|
+
}
|
|
1557
|
+
const response = await fetch(`https://sonarcloud.io/api/measures/component?component=${encodeURIComponent(sonarCloudProjectKey)}&metricKeys=coverage`, {
|
|
1558
|
+
method: "GET",
|
|
1559
|
+
headers: {
|
|
1560
|
+
Accept: "application/json",
|
|
1561
|
+
Authorization: `Basic ${Buffer.from(token.concat(":"), "utf8").toString("base64")}`,
|
|
1562
|
+
},
|
|
1563
|
+
agent: this.config.agent,
|
|
1564
|
+
});
|
|
1565
|
+
if (response.status === 401) {
|
|
1566
|
+
process.stderr.write("Unauthorized - removing token\n");
|
|
1567
|
+
await this.tokenProvider.markInvalid();
|
|
1568
|
+
}
|
|
1569
|
+
if (response.status === 404) {
|
|
1570
|
+
process.stderr.write("Project does not exist in SonarCloud\n");
|
|
1571
|
+
return undefined;
|
|
1572
|
+
}
|
|
1573
|
+
if (!response.ok) {
|
|
1574
|
+
throw new Error(`Response from SonarCloud not OK (${response.status}): ${JSON.stringify(response)}`);
|
|
1575
|
+
}
|
|
1576
|
+
return (await response.json());
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function createSonarCloudService(props) {
|
|
1580
|
+
var _a;
|
|
1581
|
+
return new SonarCloudService({
|
|
1582
|
+
config: props.config,
|
|
1583
|
+
tokenProvider: (_a = props.tokenProvider) !== null && _a !== void 0 ? _a : new SonarCloudTokenCliProvider(),
|
|
1584
|
+
});
|
|
1585
1585
|
}
|
|
1586
1586
|
|
|
1587
1587
|
var index = /*#__PURE__*/Object.freeze({
|
|
@@ -1591,455 +1591,455 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1591
1591
|
SonarCloudTokenCliProvider: SonarCloudTokenCliProvider
|
|
1592
1592
|
});
|
|
1593
1593
|
|
|
1594
|
-
class TestExecutor {
|
|
1595
|
-
constructor() {
|
|
1596
|
-
this.shutdown = false;
|
|
1597
|
-
this.cleanupTask = null;
|
|
1598
|
-
this.usingWithCleanupTasks = false;
|
|
1599
|
-
this.tasks = [];
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Check if we are currently in shutdown state due to user
|
|
1603
|
-
* asking to abort (Ctrl+C).
|
|
1604
|
-
*/
|
|
1605
|
-
checkCanContinue() {
|
|
1606
|
-
if (this.shutdown) {
|
|
1607
|
-
throw new Error("In shutdown mode - aborting");
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
async runTasks() {
|
|
1611
|
-
console.warn("Running cleanup tasks");
|
|
1612
|
-
while (true) {
|
|
1613
|
-
// We must run tasks in reverse order due to dependencies.
|
|
1614
|
-
// E.g. we cannot delete a Docker network before deleting
|
|
1615
|
-
// the container using it.
|
|
1616
|
-
const task = this.tasks.pop();
|
|
1617
|
-
if (task === undefined) {
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
try {
|
|
1621
|
-
await task();
|
|
1622
|
-
}
|
|
1623
|
-
catch (error) {
|
|
1624
|
-
console.error(error);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Register a task that will be run during cleanup phase.
|
|
1630
|
-
*/
|
|
1631
|
-
registerCleanupTask(task) {
|
|
1632
|
-
if (!this.usingWithCleanupTasks) {
|
|
1633
|
-
throw new Error("registerCleanupTask run outside runWithCleanupTasks");
|
|
1634
|
-
}
|
|
1635
|
-
this.tasks.push(task);
|
|
1636
|
-
this.checkCanContinue();
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* Run the code block while ensuring we can run cleanup tasks
|
|
1640
|
-
* after the execution or if the process is interrupted.
|
|
1641
|
-
*
|
|
1642
|
-
* The main method of the program should be executed by using
|
|
1643
|
-
* this method.
|
|
1644
|
-
*/
|
|
1645
|
-
async runWithCleanupTasks(body) {
|
|
1646
|
-
try {
|
|
1647
|
-
strict.strictEqual(this.usingWithCleanupTasks, false);
|
|
1648
|
-
this.usingWithCleanupTasks = true;
|
|
1649
|
-
// We capture Ctrl+C so that we can perform cleanup task,
|
|
1650
|
-
// since the cleanup tasks involve async code which is not
|
|
1651
|
-
// supported during NodeJS normal exit handling.
|
|
1652
|
-
//
|
|
1653
|
-
// This will not abort the running tasks until after
|
|
1654
|
-
// we have completed the cleanup tasks. The running tasks
|
|
1655
|
-
// can stop earlier by calling checkCanContinue.
|
|
1656
|
-
process.on("SIGINT", () => {
|
|
1657
|
-
console.warn("Caught interrupt signal - forcing termination");
|
|
1658
|
-
if (this.cleanupTask != null) {
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
this.shutdown = true;
|
|
1662
|
-
this.cleanupTask = this.runTasks().then(() => {
|
|
1663
|
-
process.exit(1);
|
|
1664
|
-
});
|
|
1665
|
-
});
|
|
1666
|
-
await body(this);
|
|
1667
|
-
}
|
|
1668
|
-
catch (error) {
|
|
1669
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1670
|
-
console.error(error.stack || error.message || error);
|
|
1671
|
-
process.exitCode = 1;
|
|
1672
|
-
}
|
|
1673
|
-
finally {
|
|
1674
|
-
console.log(`Reached finally block`);
|
|
1675
|
-
this.usingWithCleanupTasks = false;
|
|
1676
|
-
if (this.cleanupTask == null) {
|
|
1677
|
-
this.cleanupTask = this.runTasks();
|
|
1678
|
-
}
|
|
1679
|
-
await this.cleanupTask;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
function createTestExecutor() {
|
|
1684
|
-
return new TestExecutor();
|
|
1594
|
+
class TestExecutor {
|
|
1595
|
+
constructor() {
|
|
1596
|
+
this.shutdown = false;
|
|
1597
|
+
this.cleanupTask = null;
|
|
1598
|
+
this.usingWithCleanupTasks = false;
|
|
1599
|
+
this.tasks = [];
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Check if we are currently in shutdown state due to user
|
|
1603
|
+
* asking to abort (Ctrl+C).
|
|
1604
|
+
*/
|
|
1605
|
+
checkCanContinue() {
|
|
1606
|
+
if (this.shutdown) {
|
|
1607
|
+
throw new Error("In shutdown mode - aborting");
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async runTasks() {
|
|
1611
|
+
console.warn("Running cleanup tasks");
|
|
1612
|
+
while (true) {
|
|
1613
|
+
// We must run tasks in reverse order due to dependencies.
|
|
1614
|
+
// E.g. we cannot delete a Docker network before deleting
|
|
1615
|
+
// the container using it.
|
|
1616
|
+
const task = this.tasks.pop();
|
|
1617
|
+
if (task === undefined) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
await task();
|
|
1622
|
+
}
|
|
1623
|
+
catch (error) {
|
|
1624
|
+
console.error(error);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Register a task that will be run during cleanup phase.
|
|
1630
|
+
*/
|
|
1631
|
+
registerCleanupTask(task) {
|
|
1632
|
+
if (!this.usingWithCleanupTasks) {
|
|
1633
|
+
throw new Error("registerCleanupTask run outside runWithCleanupTasks");
|
|
1634
|
+
}
|
|
1635
|
+
this.tasks.push(task);
|
|
1636
|
+
this.checkCanContinue();
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Run the code block while ensuring we can run cleanup tasks
|
|
1640
|
+
* after the execution or if the process is interrupted.
|
|
1641
|
+
*
|
|
1642
|
+
* The main method of the program should be executed by using
|
|
1643
|
+
* this method.
|
|
1644
|
+
*/
|
|
1645
|
+
async runWithCleanupTasks(body) {
|
|
1646
|
+
try {
|
|
1647
|
+
strict.strictEqual(this.usingWithCleanupTasks, false);
|
|
1648
|
+
this.usingWithCleanupTasks = true;
|
|
1649
|
+
// We capture Ctrl+C so that we can perform cleanup task,
|
|
1650
|
+
// since the cleanup tasks involve async code which is not
|
|
1651
|
+
// supported during NodeJS normal exit handling.
|
|
1652
|
+
//
|
|
1653
|
+
// This will not abort the running tasks until after
|
|
1654
|
+
// we have completed the cleanup tasks. The running tasks
|
|
1655
|
+
// can stop earlier by calling checkCanContinue.
|
|
1656
|
+
process.on("SIGINT", () => {
|
|
1657
|
+
console.warn("Caught interrupt signal - forcing termination");
|
|
1658
|
+
if (this.cleanupTask != null) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
this.shutdown = true;
|
|
1662
|
+
this.cleanupTask = this.runTasks().then(() => {
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
await body(this);
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1670
|
+
console.error(error.stack || error.message || error);
|
|
1671
|
+
process.exitCode = 1;
|
|
1672
|
+
}
|
|
1673
|
+
finally {
|
|
1674
|
+
console.log(`Reached finally block`);
|
|
1675
|
+
this.usingWithCleanupTasks = false;
|
|
1676
|
+
if (this.cleanupTask == null) {
|
|
1677
|
+
this.cleanupTask = this.runTasks();
|
|
1678
|
+
}
|
|
1679
|
+
await this.cleanupTask;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
function createTestExecutor() {
|
|
1684
|
+
return new TestExecutor();
|
|
1685
1685
|
}
|
|
1686
1686
|
|
|
1687
|
-
/**
|
|
1688
|
-
* Generate a value that can be used as part of resource names.
|
|
1689
|
-
*
|
|
1690
|
-
* Gives a value formatted as "yyyymmdd-xxxxxx", e.g. "20200523-3f2c87".
|
|
1691
|
-
*/
|
|
1692
|
-
function generateRunId() {
|
|
1693
|
-
const low = parseInt("100000", 16);
|
|
1694
|
-
const high = parseInt("ffffff", 16);
|
|
1695
|
-
const range = high - low + 1;
|
|
1696
|
-
const now = new Date();
|
|
1697
|
-
return [
|
|
1698
|
-
now.getUTCFullYear(),
|
|
1699
|
-
(now.getUTCMonth() + 1).toString().padStart(2, "0"),
|
|
1700
|
-
now.getUTCDate().toString().padStart(2, "0"),
|
|
1701
|
-
"-",
|
|
1702
|
-
(Math.floor(Math.random() * range) + low).toString(16),
|
|
1703
|
-
].join("");
|
|
1704
|
-
}
|
|
1705
|
-
/**
|
|
1706
|
-
* Generate a name that can be used for a resource.
|
|
1707
|
-
*/
|
|
1708
|
-
function generateName(extra) {
|
|
1709
|
-
const s = extra === undefined ? "" : `-${extra}`;
|
|
1710
|
-
return `autotest-${generateRunId()}${s}`;
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Create a new Docker network.
|
|
1714
|
-
*/
|
|
1715
|
-
async function createNetwork(executor) {
|
|
1716
|
-
executor.checkCanContinue();
|
|
1717
|
-
const networkName = generateName();
|
|
1718
|
-
await execa("docker", ["network", "create", networkName]);
|
|
1719
|
-
const lsRes = await execa("docker", [
|
|
1720
|
-
"network",
|
|
1721
|
-
"ls",
|
|
1722
|
-
"-q",
|
|
1723
|
-
"-f",
|
|
1724
|
-
`name=${networkName}`,
|
|
1725
|
-
]);
|
|
1726
|
-
const networkId = lsRes.stdout.trim();
|
|
1727
|
-
console.log(`Network ${networkName} (${networkId}) created`);
|
|
1728
|
-
executor.registerCleanupTask(async () => {
|
|
1729
|
-
await execa("docker", ["network", "rm", networkId]);
|
|
1730
|
-
console.log(`Network ${networkName} (${networkId}) deleted`);
|
|
1731
|
-
});
|
|
1732
|
-
return {
|
|
1733
|
-
id: networkId,
|
|
1734
|
-
};
|
|
1735
|
-
}
|
|
1736
|
-
/**
|
|
1737
|
-
* Execute curl within the Docker network.
|
|
1738
|
-
*/
|
|
1739
|
-
async function curl(executor, network, ...args) {
|
|
1740
|
-
executor.checkCanContinue();
|
|
1741
|
-
const result = await execa("docker", [
|
|
1742
|
-
"run",
|
|
1743
|
-
"-i",
|
|
1744
|
-
"--rm",
|
|
1745
|
-
"--network",
|
|
1746
|
-
network.id,
|
|
1747
|
-
"byrnedo/alpine-curl",
|
|
1748
|
-
...args,
|
|
1749
|
-
]);
|
|
1750
|
-
return result.stdout;
|
|
1751
|
-
}
|
|
1752
|
-
/**
|
|
1753
|
-
* Repeatedly check for a condition until timeout.
|
|
1754
|
-
*
|
|
1755
|
-
* The condition can throw an error without aborting the loop.
|
|
1756
|
-
* To abort the condition must return false.
|
|
1757
|
-
*/
|
|
1758
|
-
async function pollForCondition({ container, attempts, waitIntervalSec, condition, }) {
|
|
1759
|
-
function log(value) {
|
|
1760
|
-
console.log(`${container.name} (poll): ${value}`);
|
|
1761
|
-
}
|
|
1762
|
-
container.executor.checkCanContinue();
|
|
1763
|
-
log(`Waiting for condition.. Checking ${attempts} times by ${waitIntervalSec} sec`);
|
|
1764
|
-
const start = performance.now();
|
|
1765
|
-
const duration = () => {
|
|
1766
|
-
const end = performance.now();
|
|
1767
|
-
return Math.round((end - start) / 1000);
|
|
1768
|
-
};
|
|
1769
|
-
for (let i = 0; i < attempts; i++) {
|
|
1770
|
-
container.executor.checkCanContinue();
|
|
1771
|
-
if (!(await isRunning(container.executor, container))) {
|
|
1772
|
-
throw new Error(`Container ${container.name} not running as expected`);
|
|
1773
|
-
}
|
|
1774
|
-
try {
|
|
1775
|
-
const result = await condition();
|
|
1776
|
-
if (!result) {
|
|
1777
|
-
break;
|
|
1778
|
-
}
|
|
1779
|
-
log(`Took ${duration()} seconds for condition`);
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
catch (e) {
|
|
1783
|
-
log("Still waiting...");
|
|
1784
|
-
await new Promise((resolve) => setTimeout(resolve, waitIntervalSec * 1000));
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
throw new Error(`Failed to wait for container ${container.name}`);
|
|
1788
|
-
}
|
|
1789
|
-
async function waitForHttpOk({ container, url, attempts = 30, waitIntervalSec = 1, }) {
|
|
1790
|
-
await pollForCondition({
|
|
1791
|
-
container,
|
|
1792
|
-
attempts,
|
|
1793
|
-
waitIntervalSec,
|
|
1794
|
-
condition: async () => {
|
|
1795
|
-
await curl(container.executor, container.network, "-fsS", url);
|
|
1796
|
-
return true;
|
|
1797
|
-
},
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
async function waitForPostgresAvailable({ container, attempts = 30, waitIntervalSec = 1, username = "user", password = "password", dbname, }) {
|
|
1801
|
-
await pollForCondition({
|
|
1802
|
-
container,
|
|
1803
|
-
attempts,
|
|
1804
|
-
waitIntervalSec,
|
|
1805
|
-
condition: async () => {
|
|
1806
|
-
await execa("docker", [
|
|
1807
|
-
"exec",
|
|
1808
|
-
"-e",
|
|
1809
|
-
`PGPASSWORD=${password}`,
|
|
1810
|
-
container.name,
|
|
1811
|
-
"psql",
|
|
1812
|
-
"-h",
|
|
1813
|
-
"localhost",
|
|
1814
|
-
"-U",
|
|
1815
|
-
username,
|
|
1816
|
-
"-c",
|
|
1817
|
-
"select 1",
|
|
1818
|
-
dbname,
|
|
1819
|
-
]);
|
|
1820
|
-
return true;
|
|
1821
|
-
},
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
1824
|
-
async function isRunning(executor, container) {
|
|
1825
|
-
executor.checkCanContinue();
|
|
1826
|
-
try {
|
|
1827
|
-
await execa("docker", ["inspect", container.name]);
|
|
1828
|
-
return true;
|
|
1829
|
-
}
|
|
1830
|
-
catch (e) {
|
|
1831
|
-
return false;
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
/**
|
|
1835
|
-
* A stream transform that injects a prefix into every line
|
|
1836
|
-
* and also forces every chunk to end with a newline so that
|
|
1837
|
-
* it can be interleaved with other output.
|
|
1838
|
-
*/
|
|
1839
|
-
class OutputPrefixTransform extends Transform {
|
|
1840
|
-
constructor(prefix) {
|
|
1841
|
-
super({
|
|
1842
|
-
objectMode: true,
|
|
1843
|
-
transform: (chunk, encoding, callback) => {
|
|
1844
|
-
let result = chunk.toString(encoding);
|
|
1845
|
-
if (result.endsWith("\n")) {
|
|
1846
|
-
result = result.slice(0, -1);
|
|
1847
|
-
}
|
|
1848
|
-
// Some loggers emit newline then ANSI reset code causing
|
|
1849
|
-
// blank lines if we do not remove the newline.
|
|
1850
|
-
// TODO: Consider removing all ANSI escape codes as it causes
|
|
1851
|
-
// some confusing output when interleaved.
|
|
1852
|
-
if (result.endsWith("\n\u001B[0m")) {
|
|
1853
|
-
result = result.slice(0, -5) + result.slice(-4);
|
|
1854
|
-
}
|
|
1855
|
-
result = prefix + result.replace(/\n/g, `\n${prefix}`) + "\n";
|
|
1856
|
-
callback(null, result);
|
|
1857
|
-
},
|
|
1858
|
-
});
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
function pipeToConsole(result, name) {
|
|
1862
|
-
var _a, _b;
|
|
1863
|
-
(_a = result.stdout) === null || _a === void 0 ? void 0 : _a.pipe(new OutputPrefixTransform(`${name}: `)).pipe(process.stdout);
|
|
1864
|
-
(_b = result.stderr) === null || _b === void 0 ? void 0 : _b.pipe(new OutputPrefixTransform(`${name} (stderr): `)).pipe(process.stderr);
|
|
1865
|
-
}
|
|
1866
|
-
function checkPidRunning(pid) {
|
|
1867
|
-
try {
|
|
1868
|
-
process.kill(pid, 0);
|
|
1869
|
-
return true;
|
|
1870
|
-
}
|
|
1871
|
-
catch (e) {
|
|
1872
|
-
return false;
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
async function getContainerId({ executor, name, hasFailed, pid, }) {
|
|
1876
|
-
function log(value) {
|
|
1877
|
-
console.log(`${name} (get-container-id): ${value}`);
|
|
1878
|
-
}
|
|
1879
|
-
async function check() {
|
|
1880
|
-
let result;
|
|
1881
|
-
try {
|
|
1882
|
-
result = (await execa("docker", ["inspect", name, "-f", "{{.Id}}"]))
|
|
1883
|
-
.stdout;
|
|
1884
|
-
}
|
|
1885
|
-
catch (e) {
|
|
1886
|
-
result = null;
|
|
1887
|
-
}
|
|
1888
|
-
// Debugging to help us solve CALS-366.
|
|
1889
|
-
const ps = execa("docker", ["ps"]);
|
|
1890
|
-
pipeToConsole(ps, `${name} (ps)`);
|
|
1891
|
-
await ps;
|
|
1892
|
-
// Debugging to help us solve CALS-366.
|
|
1893
|
-
if (!checkPidRunning(pid)) {
|
|
1894
|
-
log("Process not running");
|
|
1895
|
-
}
|
|
1896
|
-
return result;
|
|
1897
|
-
}
|
|
1898
|
-
// If the container is not running, retry a few times to cover
|
|
1899
|
-
// the initial starting where we might check before the container
|
|
1900
|
-
// is running.
|
|
1901
|
-
// Increased from 25 to 100 to see if it helps for solving CALS-366.
|
|
1902
|
-
for (let i = 0; i < 100; i++) {
|
|
1903
|
-
if (i > 0) {
|
|
1904
|
-
// Delay a bit before checking again.
|
|
1905
|
-
log("Retrying in a bit...");
|
|
1906
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1907
|
-
}
|
|
1908
|
-
executor.checkCanContinue();
|
|
1909
|
-
if (hasFailed()) {
|
|
1910
|
-
break;
|
|
1911
|
-
}
|
|
1912
|
-
const id = await check();
|
|
1913
|
-
if (id !== null) {
|
|
1914
|
-
log(`Resolved to ${id}`);
|
|
1915
|
-
return id;
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
throw new Error(`Could not find ID for container with name ${name}`);
|
|
1919
|
-
}
|
|
1920
|
-
async function pullImage({ imageId }) {
|
|
1921
|
-
console.log(`Pulling ${imageId}`);
|
|
1922
|
-
const process = execa("docker", ["pull", imageId]);
|
|
1923
|
-
pipeToConsole(process, `pull-image (${imageId})`);
|
|
1924
|
-
await process;
|
|
1925
|
-
}
|
|
1926
|
-
async function checkImageExistsLocally({ imageId, }) {
|
|
1927
|
-
const result = await execa("docker", ["images", "-q", imageId]);
|
|
1928
|
-
const found = result.stdout != "";
|
|
1929
|
-
console.log(`image ${imageId} ${found ? "was present locally" : "was not found locally"}`);
|
|
1930
|
-
return found;
|
|
1931
|
-
}
|
|
1932
|
-
async function startContainer({ executor, network, imageId, alias, env, dockerArgs = [], pull = false, }) {
|
|
1933
|
-
executor.checkCanContinue();
|
|
1934
|
-
const containerName = generateName(alias);
|
|
1935
|
-
// Prefer pulling image here so that the call on getContainerId
|
|
1936
|
-
// will not time out due to pulling the image.
|
|
1937
|
-
// If pull is false, we will still fallback to pulling if we cannot
|
|
1938
|
-
// find the image locally.
|
|
1939
|
-
if (pull || !(await checkImageExistsLocally({ imageId }))) {
|
|
1940
|
-
await pullImage({
|
|
1941
|
-
imageId,
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
const args = [
|
|
1945
|
-
"run",
|
|
1946
|
-
"--rm",
|
|
1947
|
-
"--network",
|
|
1948
|
-
network.id,
|
|
1949
|
-
"--name",
|
|
1950
|
-
containerName,
|
|
1951
|
-
...dockerArgs,
|
|
1952
|
-
];
|
|
1953
|
-
if (alias != null) {
|
|
1954
|
-
args.push(`--network-alias=${alias}`);
|
|
1955
|
-
}
|
|
1956
|
-
if (env != null) {
|
|
1957
|
-
for (const [key, value] of Object.entries(env)) {
|
|
1958
|
-
args.push("-e", `${key}=${value}`);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
args.push(imageId);
|
|
1962
|
-
console.log(`Starting ${imageId}`);
|
|
1963
|
-
const process = execa("docker", args);
|
|
1964
|
-
pipeToConsole(process, alias !== null && alias !== void 0 ? alias : containerName);
|
|
1965
|
-
let failed = false;
|
|
1966
|
-
process.catch(() => {
|
|
1967
|
-
failed = true;
|
|
1968
|
-
});
|
|
1969
|
-
if (!process.pid) {
|
|
1970
|
-
throw new Error("No process identifier (PID) was returned for the process that was started when running trying to run Docker container");
|
|
1971
|
-
}
|
|
1972
|
-
const id = await getContainerId({
|
|
1973
|
-
executor,
|
|
1974
|
-
name: containerName,
|
|
1975
|
-
hasFailed: () => failed,
|
|
1976
|
-
pid: process.pid,
|
|
1977
|
-
});
|
|
1978
|
-
executor.registerCleanupTask(async () => {
|
|
1979
|
-
console.log(`Stopping container ${containerName}`);
|
|
1980
|
-
const r = execa("docker", ["stop", containerName]);
|
|
1981
|
-
pipeToConsole(r, (alias !== null && alias !== void 0 ? alias : containerName) + " (stop)");
|
|
1982
|
-
try {
|
|
1983
|
-
await r;
|
|
1984
|
-
}
|
|
1985
|
-
catch (e) {
|
|
1986
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
1987
|
-
if (!(e.stderr || "").includes("No such container")) {
|
|
1988
|
-
throw e;
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
});
|
|
1992
|
-
return {
|
|
1993
|
-
id,
|
|
1994
|
-
name: containerName,
|
|
1995
|
-
network,
|
|
1996
|
-
process,
|
|
1997
|
-
executor,
|
|
1998
|
-
};
|
|
1999
|
-
}
|
|
2000
|
-
async function runNpmRunScript(name, options) {
|
|
2001
|
-
const result = execa("npm", ["run", name], {
|
|
2002
|
-
env: options === null || options === void 0 ? void 0 : options.env,
|
|
2003
|
-
});
|
|
2004
|
-
pipeToConsole(result, `npm run ${name}`);
|
|
2005
|
-
await result;
|
|
2006
|
-
}
|
|
2007
|
-
/**
|
|
2008
|
-
* This likely does not cover all situations.
|
|
2009
|
-
*/
|
|
2010
|
-
async function getDockerHostAddress() {
|
|
2011
|
-
if (process.platform === "darwin" || process.platform === "win32") {
|
|
2012
|
-
return "host.docker.internal";
|
|
2013
|
-
}
|
|
2014
|
-
if (fs.existsSync("/.dockerenv")) {
|
|
2015
|
-
const process = execa("ip", ["route"]);
|
|
2016
|
-
pipeToConsole(process, "ip route");
|
|
2017
|
-
const res = await process;
|
|
2018
|
-
try {
|
|
2019
|
-
return (res.stdout
|
|
2020
|
-
.split("\n")
|
|
2021
|
-
.filter((it) => it.includes("default via"))
|
|
2022
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2023
|
-
.map((it) => /default via ([\d\.]+) /.exec(it)[1])[0]);
|
|
2024
|
-
}
|
|
2025
|
-
catch (e) {
|
|
2026
|
-
throw new Error("Failed to extract docker host address");
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
return "localhost";
|
|
2030
|
-
}
|
|
2031
|
-
async function waitForEnterToContinue(prompt = "Press enter to continue") {
|
|
2032
|
-
return new Promise((resolve, reject) => {
|
|
2033
|
-
read({
|
|
2034
|
-
prompt,
|
|
2035
|
-
silent: true,
|
|
2036
|
-
}, (err) => {
|
|
2037
|
-
if (err) {
|
|
2038
|
-
reject(err);
|
|
2039
|
-
}
|
|
2040
|
-
resolve();
|
|
2041
|
-
});
|
|
2042
|
-
});
|
|
1687
|
+
/**
|
|
1688
|
+
* Generate a value that can be used as part of resource names.
|
|
1689
|
+
*
|
|
1690
|
+
* Gives a value formatted as "yyyymmdd-xxxxxx", e.g. "20200523-3f2c87".
|
|
1691
|
+
*/
|
|
1692
|
+
function generateRunId() {
|
|
1693
|
+
const low = parseInt("100000", 16);
|
|
1694
|
+
const high = parseInt("ffffff", 16);
|
|
1695
|
+
const range = high - low + 1;
|
|
1696
|
+
const now = new Date();
|
|
1697
|
+
return [
|
|
1698
|
+
now.getUTCFullYear(),
|
|
1699
|
+
(now.getUTCMonth() + 1).toString().padStart(2, "0"),
|
|
1700
|
+
now.getUTCDate().toString().padStart(2, "0"),
|
|
1701
|
+
"-",
|
|
1702
|
+
(Math.floor(Math.random() * range) + low).toString(16),
|
|
1703
|
+
].join("");
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Generate a name that can be used for a resource.
|
|
1707
|
+
*/
|
|
1708
|
+
function generateName(extra) {
|
|
1709
|
+
const s = extra === undefined ? "" : `-${extra}`;
|
|
1710
|
+
return `autotest-${generateRunId()}${s}`;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Create a new Docker network.
|
|
1714
|
+
*/
|
|
1715
|
+
async function createNetwork(executor) {
|
|
1716
|
+
executor.checkCanContinue();
|
|
1717
|
+
const networkName = generateName();
|
|
1718
|
+
await execa("docker", ["network", "create", networkName]);
|
|
1719
|
+
const lsRes = await execa("docker", [
|
|
1720
|
+
"network",
|
|
1721
|
+
"ls",
|
|
1722
|
+
"-q",
|
|
1723
|
+
"-f",
|
|
1724
|
+
`name=${networkName}`,
|
|
1725
|
+
]);
|
|
1726
|
+
const networkId = lsRes.stdout.trim();
|
|
1727
|
+
console.log(`Network ${networkName} (${networkId}) created`);
|
|
1728
|
+
executor.registerCleanupTask(async () => {
|
|
1729
|
+
await execa("docker", ["network", "rm", networkId]);
|
|
1730
|
+
console.log(`Network ${networkName} (${networkId}) deleted`);
|
|
1731
|
+
});
|
|
1732
|
+
return {
|
|
1733
|
+
id: networkId,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Execute curl within the Docker network.
|
|
1738
|
+
*/
|
|
1739
|
+
async function curl(executor, network, ...args) {
|
|
1740
|
+
executor.checkCanContinue();
|
|
1741
|
+
const result = await execa("docker", [
|
|
1742
|
+
"run",
|
|
1743
|
+
"-i",
|
|
1744
|
+
"--rm",
|
|
1745
|
+
"--network",
|
|
1746
|
+
network.id,
|
|
1747
|
+
"byrnedo/alpine-curl",
|
|
1748
|
+
...args,
|
|
1749
|
+
]);
|
|
1750
|
+
return result.stdout;
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Repeatedly check for a condition until timeout.
|
|
1754
|
+
*
|
|
1755
|
+
* The condition can throw an error without aborting the loop.
|
|
1756
|
+
* To abort the condition must return false.
|
|
1757
|
+
*/
|
|
1758
|
+
async function pollForCondition({ container, attempts, waitIntervalSec, condition, }) {
|
|
1759
|
+
function log(value) {
|
|
1760
|
+
console.log(`${container.name} (poll): ${value}`);
|
|
1761
|
+
}
|
|
1762
|
+
container.executor.checkCanContinue();
|
|
1763
|
+
log(`Waiting for condition.. Checking ${attempts} times by ${waitIntervalSec} sec`);
|
|
1764
|
+
const start = performance.now();
|
|
1765
|
+
const duration = () => {
|
|
1766
|
+
const end = performance.now();
|
|
1767
|
+
return Math.round((end - start) / 1000);
|
|
1768
|
+
};
|
|
1769
|
+
for (let i = 0; i < attempts; i++) {
|
|
1770
|
+
container.executor.checkCanContinue();
|
|
1771
|
+
if (!(await isRunning(container.executor, container))) {
|
|
1772
|
+
throw new Error(`Container ${container.name} not running as expected`);
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
const result = await condition();
|
|
1776
|
+
if (!result) {
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
log(`Took ${duration()} seconds for condition`);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
catch (e) {
|
|
1783
|
+
log("Still waiting...");
|
|
1784
|
+
await new Promise((resolve) => setTimeout(resolve, waitIntervalSec * 1000));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
throw new Error(`Failed to wait for container ${container.name}`);
|
|
1788
|
+
}
|
|
1789
|
+
async function waitForHttpOk({ container, url, attempts = 30, waitIntervalSec = 1, }) {
|
|
1790
|
+
await pollForCondition({
|
|
1791
|
+
container,
|
|
1792
|
+
attempts,
|
|
1793
|
+
waitIntervalSec,
|
|
1794
|
+
condition: async () => {
|
|
1795
|
+
await curl(container.executor, container.network, "-fsS", url);
|
|
1796
|
+
return true;
|
|
1797
|
+
},
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
async function waitForPostgresAvailable({ container, attempts = 30, waitIntervalSec = 1, username = "user", password = "password", dbname, }) {
|
|
1801
|
+
await pollForCondition({
|
|
1802
|
+
container,
|
|
1803
|
+
attempts,
|
|
1804
|
+
waitIntervalSec,
|
|
1805
|
+
condition: async () => {
|
|
1806
|
+
await execa("docker", [
|
|
1807
|
+
"exec",
|
|
1808
|
+
"-e",
|
|
1809
|
+
`PGPASSWORD=${password}`,
|
|
1810
|
+
container.name,
|
|
1811
|
+
"psql",
|
|
1812
|
+
"-h",
|
|
1813
|
+
"localhost",
|
|
1814
|
+
"-U",
|
|
1815
|
+
username,
|
|
1816
|
+
"-c",
|
|
1817
|
+
"select 1",
|
|
1818
|
+
dbname,
|
|
1819
|
+
]);
|
|
1820
|
+
return true;
|
|
1821
|
+
},
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
async function isRunning(executor, container) {
|
|
1825
|
+
executor.checkCanContinue();
|
|
1826
|
+
try {
|
|
1827
|
+
await execa("docker", ["inspect", container.name]);
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
catch (e) {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* A stream transform that injects a prefix into every line
|
|
1836
|
+
* and also forces every chunk to end with a newline so that
|
|
1837
|
+
* it can be interleaved with other output.
|
|
1838
|
+
*/
|
|
1839
|
+
class OutputPrefixTransform extends Transform {
|
|
1840
|
+
constructor(prefix) {
|
|
1841
|
+
super({
|
|
1842
|
+
objectMode: true,
|
|
1843
|
+
transform: (chunk, encoding, callback) => {
|
|
1844
|
+
let result = chunk.toString(encoding);
|
|
1845
|
+
if (result.endsWith("\n")) {
|
|
1846
|
+
result = result.slice(0, -1);
|
|
1847
|
+
}
|
|
1848
|
+
// Some loggers emit newline then ANSI reset code causing
|
|
1849
|
+
// blank lines if we do not remove the newline.
|
|
1850
|
+
// TODO: Consider removing all ANSI escape codes as it causes
|
|
1851
|
+
// some confusing output when interleaved.
|
|
1852
|
+
if (result.endsWith("\n\u001B[0m")) {
|
|
1853
|
+
result = result.slice(0, -5) + result.slice(-4);
|
|
1854
|
+
}
|
|
1855
|
+
result = prefix + result.replace(/\n/g, `\n${prefix}`) + "\n";
|
|
1856
|
+
callback(null, result);
|
|
1857
|
+
},
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function pipeToConsole(result, name) {
|
|
1862
|
+
var _a, _b;
|
|
1863
|
+
(_a = result.stdout) === null || _a === void 0 ? void 0 : _a.pipe(new OutputPrefixTransform(`${name}: `)).pipe(process.stdout);
|
|
1864
|
+
(_b = result.stderr) === null || _b === void 0 ? void 0 : _b.pipe(new OutputPrefixTransform(`${name} (stderr): `)).pipe(process.stderr);
|
|
1865
|
+
}
|
|
1866
|
+
function checkPidRunning(pid) {
|
|
1867
|
+
try {
|
|
1868
|
+
process.kill(pid, 0);
|
|
1869
|
+
return true;
|
|
1870
|
+
}
|
|
1871
|
+
catch (e) {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
async function getContainerId({ executor, name, hasFailed, pid, }) {
|
|
1876
|
+
function log(value) {
|
|
1877
|
+
console.log(`${name} (get-container-id): ${value}`);
|
|
1878
|
+
}
|
|
1879
|
+
async function check() {
|
|
1880
|
+
let result;
|
|
1881
|
+
try {
|
|
1882
|
+
result = (await execa("docker", ["inspect", name, "-f", "{{.Id}}"]))
|
|
1883
|
+
.stdout;
|
|
1884
|
+
}
|
|
1885
|
+
catch (e) {
|
|
1886
|
+
result = null;
|
|
1887
|
+
}
|
|
1888
|
+
// Debugging to help us solve CALS-366.
|
|
1889
|
+
const ps = execa("docker", ["ps"]);
|
|
1890
|
+
pipeToConsole(ps, `${name} (ps)`);
|
|
1891
|
+
await ps;
|
|
1892
|
+
// Debugging to help us solve CALS-366.
|
|
1893
|
+
if (!checkPidRunning(pid)) {
|
|
1894
|
+
log("Process not running");
|
|
1895
|
+
}
|
|
1896
|
+
return result;
|
|
1897
|
+
}
|
|
1898
|
+
// If the container is not running, retry a few times to cover
|
|
1899
|
+
// the initial starting where we might check before the container
|
|
1900
|
+
// is running.
|
|
1901
|
+
// Increased from 25 to 100 to see if it helps for solving CALS-366.
|
|
1902
|
+
for (let i = 0; i < 100; i++) {
|
|
1903
|
+
if (i > 0) {
|
|
1904
|
+
// Delay a bit before checking again.
|
|
1905
|
+
log("Retrying in a bit...");
|
|
1906
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1907
|
+
}
|
|
1908
|
+
executor.checkCanContinue();
|
|
1909
|
+
if (hasFailed()) {
|
|
1910
|
+
break;
|
|
1911
|
+
}
|
|
1912
|
+
const id = await check();
|
|
1913
|
+
if (id !== null) {
|
|
1914
|
+
log(`Resolved to ${id}`);
|
|
1915
|
+
return id;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
throw new Error(`Could not find ID for container with name ${name}`);
|
|
1919
|
+
}
|
|
1920
|
+
async function pullImage({ imageId }) {
|
|
1921
|
+
console.log(`Pulling ${imageId}`);
|
|
1922
|
+
const process = execa("docker", ["pull", imageId]);
|
|
1923
|
+
pipeToConsole(process, `pull-image (${imageId})`);
|
|
1924
|
+
await process;
|
|
1925
|
+
}
|
|
1926
|
+
async function checkImageExistsLocally({ imageId, }) {
|
|
1927
|
+
const result = await execa("docker", ["images", "-q", imageId]);
|
|
1928
|
+
const found = result.stdout != "";
|
|
1929
|
+
console.log(`image ${imageId} ${found ? "was present locally" : "was not found locally"}`);
|
|
1930
|
+
return found;
|
|
1931
|
+
}
|
|
1932
|
+
async function startContainer({ executor, network, imageId, alias, env, dockerArgs = [], pull = false, }) {
|
|
1933
|
+
executor.checkCanContinue();
|
|
1934
|
+
const containerName = generateName(alias);
|
|
1935
|
+
// Prefer pulling image here so that the call on getContainerId
|
|
1936
|
+
// will not time out due to pulling the image.
|
|
1937
|
+
// If pull is false, we will still fallback to pulling if we cannot
|
|
1938
|
+
// find the image locally.
|
|
1939
|
+
if (pull || !(await checkImageExistsLocally({ imageId }))) {
|
|
1940
|
+
await pullImage({
|
|
1941
|
+
imageId,
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
const args = [
|
|
1945
|
+
"run",
|
|
1946
|
+
"--rm",
|
|
1947
|
+
"--network",
|
|
1948
|
+
network.id,
|
|
1949
|
+
"--name",
|
|
1950
|
+
containerName,
|
|
1951
|
+
...dockerArgs,
|
|
1952
|
+
];
|
|
1953
|
+
if (alias != null) {
|
|
1954
|
+
args.push(`--network-alias=${alias}`);
|
|
1955
|
+
}
|
|
1956
|
+
if (env != null) {
|
|
1957
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1958
|
+
args.push("-e", `${key}=${value}`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
args.push(imageId);
|
|
1962
|
+
console.log(`Starting ${imageId}`);
|
|
1963
|
+
const process = execa("docker", args);
|
|
1964
|
+
pipeToConsole(process, alias !== null && alias !== void 0 ? alias : containerName);
|
|
1965
|
+
let failed = false;
|
|
1966
|
+
process.catch(() => {
|
|
1967
|
+
failed = true;
|
|
1968
|
+
});
|
|
1969
|
+
if (!process.pid) {
|
|
1970
|
+
throw new Error("No process identifier (PID) was returned for the process that was started when running trying to run Docker container");
|
|
1971
|
+
}
|
|
1972
|
+
const id = await getContainerId({
|
|
1973
|
+
executor,
|
|
1974
|
+
name: containerName,
|
|
1975
|
+
hasFailed: () => failed,
|
|
1976
|
+
pid: process.pid,
|
|
1977
|
+
});
|
|
1978
|
+
executor.registerCleanupTask(async () => {
|
|
1979
|
+
console.log(`Stopping container ${containerName}`);
|
|
1980
|
+
const r = execa("docker", ["stop", containerName]);
|
|
1981
|
+
pipeToConsole(r, (alias !== null && alias !== void 0 ? alias : containerName) + " (stop)");
|
|
1982
|
+
try {
|
|
1983
|
+
await r;
|
|
1984
|
+
}
|
|
1985
|
+
catch (e) {
|
|
1986
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
1987
|
+
if (!(e.stderr || "").includes("No such container")) {
|
|
1988
|
+
throw e;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
return {
|
|
1993
|
+
id,
|
|
1994
|
+
name: containerName,
|
|
1995
|
+
network,
|
|
1996
|
+
process,
|
|
1997
|
+
executor,
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
async function runNpmRunScript(name, options) {
|
|
2001
|
+
const result = execa("npm", ["run", name], {
|
|
2002
|
+
env: options === null || options === void 0 ? void 0 : options.env,
|
|
2003
|
+
});
|
|
2004
|
+
pipeToConsole(result, `npm run ${name}`);
|
|
2005
|
+
await result;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* This likely does not cover all situations.
|
|
2009
|
+
*/
|
|
2010
|
+
async function getDockerHostAddress() {
|
|
2011
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
2012
|
+
return "host.docker.internal";
|
|
2013
|
+
}
|
|
2014
|
+
if (fs.existsSync("/.dockerenv")) {
|
|
2015
|
+
const process = execa("ip", ["route"]);
|
|
2016
|
+
pipeToConsole(process, "ip route");
|
|
2017
|
+
const res = await process;
|
|
2018
|
+
try {
|
|
2019
|
+
return (res.stdout
|
|
2020
|
+
.split("\n")
|
|
2021
|
+
.filter((it) => it.includes("default via"))
|
|
2022
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2023
|
+
.map((it) => /default via ([\d\.]+) /.exec(it)[1])[0]);
|
|
2024
|
+
}
|
|
2025
|
+
catch (e) {
|
|
2026
|
+
throw new Error("Failed to extract docker host address");
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return "localhost";
|
|
2030
|
+
}
|
|
2031
|
+
async function waitForEnterToContinue(prompt = "Press enter to continue") {
|
|
2032
|
+
return new Promise((resolve, reject) => {
|
|
2033
|
+
read({
|
|
2034
|
+
prompt,
|
|
2035
|
+
silent: true,
|
|
2036
|
+
}, (err) => {
|
|
2037
|
+
if (err) {
|
|
2038
|
+
reject(err);
|
|
2039
|
+
}
|
|
2040
|
+
resolve();
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
2043
2043
|
}
|
|
2044
2044
|
|
|
2045
2045
|
const VERSION = version;
|