trustci 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/cli.js +221 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ npx trustci list
|
|
|
32
32
|
| ---------- | ---------------------------------------------------------------------------------------------- | --------------- |
|
|
33
33
|
| `github` | `--repo <owner/repo>` `--file <workflow>` | `--env <name>` |
|
|
34
34
|
| `gitlab` | `--project <group/project>` (or `--repo`) `--file <workflow>` | `--env <name>` |
|
|
35
|
-
| `circleci` | `--org-id` `--project-id` `--pipeline-definition-id
|
|
35
|
+
| `circleci` | Either all of `--org-id` `--project-id` `--pipeline-definition-id`, or a CircleCI token | `--vcs-origin <host/owner/repo>`, `--circle-token`, `--file`, `--context-id` (repeatable) |
|
|
36
36
|
|
|
37
37
|
At least one of `--allow-publish` / `--allow-stage-publish` is required.
|
|
38
38
|
|
|
@@ -48,6 +48,31 @@ npx trustci --provider circleci \
|
|
|
48
48
|
--allow-publish
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
#### CircleCI `--vcs-origin`
|
|
52
|
+
|
|
53
|
+
When `--vcs-origin` is omitted, trustci takes it from the root `package.json`
|
|
54
|
+
`repository` field (URL, `git+https://…`, `git@host:owner/repo`, or the
|
|
55
|
+
`github:` / `gitlab:` / `bitbucket:` / `owner/repo` shorthands), then from
|
|
56
|
+
`git remote get-url origin`. It prints which one it used, and warns when the two
|
|
57
|
+
disagree (`package.json` wins, since it is committed rather than clone-specific).
|
|
58
|
+
|
|
59
|
+
#### CircleCI token lookup
|
|
60
|
+
|
|
61
|
+
When any of the three UUIDs is missing, trustci resolves it through the CircleCI
|
|
62
|
+
API v2. The token is taken from the first of these that is set, and trustci
|
|
63
|
+
prints which one it used (never the token itself):
|
|
64
|
+
|
|
65
|
+
1. `--circle-token`
|
|
66
|
+
2. `CIRCLE_TOKEN` (circleci CLI v1, chunk)
|
|
67
|
+
3. `CIRCLECI_TOKEN` (chunk)
|
|
68
|
+
4. `CIRCLE_CLI_TOKEN` (circleci CLI v1, legacy alias)
|
|
69
|
+
5. `CIRCLECI_CLI_TOKEN` (circleci CLI v0.1)
|
|
70
|
+
6. `token` in `~/.circleci/cli.yml`, written by `circleci setup` in circleci CLI v0.1.
|
|
71
|
+
It is ignored when its `host` is not `https://circleci.com`.
|
|
72
|
+
|
|
73
|
+
The OS keychain, where circleci CLI v1 and chunk store tokens by default, is not read.
|
|
74
|
+
When all three UUIDs are given, no token is looked up.
|
|
75
|
+
|
|
51
76
|
### Common options
|
|
52
77
|
|
|
53
78
|
| Flag | Description |
|
package/dist/cli.js
CHANGED
|
@@ -37,13 +37,24 @@ function projectSlugFromVcsOrigin(origin) {
|
|
|
37
37
|
}
|
|
38
38
|
return `${vcsType}/${parts[0]}/${parts[1]}`;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
class CircleApiError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
path;
|
|
44
|
+
body;
|
|
45
|
+
constructor(status, path, body) {
|
|
46
|
+
super(`CircleCI API error (${status}) for ${path}: ${body}`);
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.path = path;
|
|
49
|
+
this.body = body;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
40
52
|
async function circleGet(token, path) {
|
|
41
53
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
42
54
|
headers: { "Circle-Token": token, Accept: "application/json" }
|
|
43
55
|
});
|
|
44
56
|
if (!response.ok) {
|
|
45
|
-
|
|
46
|
-
throw new Error(`CircleCI API error (${response.status}) for ${path}: ${body}`);
|
|
57
|
+
throw new CircleApiError(response.status, path, await response.text());
|
|
47
58
|
}
|
|
48
59
|
return response.json();
|
|
49
60
|
}
|
|
@@ -168,26 +179,17 @@ function parseCliArgs(argv) {
|
|
|
168
179
|
break;
|
|
169
180
|
}
|
|
170
181
|
case "circleci": {
|
|
182
|
+
const rawVcsOrigin = values["vcs-origin"];
|
|
171
183
|
let vcsOrigin;
|
|
172
184
|
try {
|
|
173
|
-
vcsOrigin = normalizeVcsOrigin(
|
|
185
|
+
vcsOrigin = rawVcsOrigin === undefined ? undefined : normalizeVcsOrigin(rawVcsOrigin);
|
|
174
186
|
} catch (e) {
|
|
175
|
-
if (e instanceof ArgError)
|
|
176
|
-
throw e;
|
|
177
187
|
throw new ArgError(`--${e.message}`);
|
|
178
188
|
}
|
|
179
189
|
const circleToken = values["circle-token"];
|
|
180
190
|
const orgId = values["org-id"];
|
|
181
191
|
const projectId = values["project-id"];
|
|
182
192
|
const pipelineDefinitionId = values["pipeline-definition-id"];
|
|
183
|
-
if (!circleToken) {
|
|
184
|
-
if (!orgId)
|
|
185
|
-
throw new ArgError("--org-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
186
|
-
if (!projectId)
|
|
187
|
-
throw new ArgError("--project-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
188
|
-
if (!pipelineDefinitionId)
|
|
189
|
-
throw new ArgError("--pipeline-definition-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
190
|
-
}
|
|
191
193
|
providerOptions = {
|
|
192
194
|
provider: "circleci",
|
|
193
195
|
circleToken,
|
|
@@ -226,27 +228,211 @@ function runInherit(cmd, args) {
|
|
|
226
228
|
return res.status ?? (res.error || res.signal ? 1 : 0);
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
// src/resolve.ts
|
|
232
|
+
import { readFileSync } from "node:fs";
|
|
233
|
+
import { homedir } from "node:os";
|
|
234
|
+
import { join as join2 } from "node:path";
|
|
235
|
+
|
|
236
|
+
// src/token.ts
|
|
237
|
+
import { join } from "node:path";
|
|
238
|
+
var DEFAULT_HOST = "https://circleci.com";
|
|
239
|
+
var LEGACY_CLI_YML = "~/.circleci/cli.yml";
|
|
240
|
+
var TOKEN_ENV_VARS = ["CIRCLE_TOKEN", "CIRCLECI_TOKEN", "CIRCLE_CLI_TOKEN", "CIRCLECI_CLI_TOKEN"];
|
|
241
|
+
var TOKEN_SOURCES = ["--circle-token", ...TOKEN_ENV_VARS, LEGACY_CLI_YML];
|
|
242
|
+
function parseLegacyCliYml(content) {
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
245
|
+
const m = /^(token|host):(.*)$/.exec(raw);
|
|
246
|
+
if (!m)
|
|
247
|
+
continue;
|
|
248
|
+
const value = parseScalar(m[2].trim());
|
|
249
|
+
if (value)
|
|
250
|
+
out[m[1]] = value;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
function parseScalar(v) {
|
|
255
|
+
const quoted = /^(["'])(.*)\1(\s+#.*)?$/.exec(v);
|
|
256
|
+
if (quoted)
|
|
257
|
+
return quoted[2];
|
|
258
|
+
return v.replace(/\s+#.*$/, "").trim();
|
|
259
|
+
}
|
|
260
|
+
function resolveCircleToken(input) {
|
|
261
|
+
if (input.flag)
|
|
262
|
+
return { token: input.flag, source: "--circle-token", notes: [] };
|
|
263
|
+
for (const name of TOKEN_ENV_VARS) {
|
|
264
|
+
const value = input.env[name];
|
|
265
|
+
if (value)
|
|
266
|
+
return { token: value, source: name, notes: [] };
|
|
267
|
+
}
|
|
268
|
+
const content = input.readFile(join(input.homedir, ".circleci", "cli.yml"));
|
|
269
|
+
if (content === null)
|
|
270
|
+
return { token: undefined, notes: [] };
|
|
271
|
+
const { token, host } = parseLegacyCliYml(content);
|
|
272
|
+
if (!token)
|
|
273
|
+
return { token: undefined, notes: [] };
|
|
274
|
+
if (token === "|" || token === ">" || /^[|>][-+0-9]*$/.test(token)) {
|
|
275
|
+
return { token: undefined, notes: [`skipped ${LEGACY_CLI_YML}: its token is not a single-line value trustci can read`] };
|
|
276
|
+
}
|
|
277
|
+
if (host && host.replace(/\/+$/, "") !== DEFAULT_HOST) {
|
|
278
|
+
return { token: undefined, notes: [`skipped ${LEGACY_CLI_YML}: its token is for ${host}, not ${DEFAULT_HOST}`] };
|
|
279
|
+
}
|
|
280
|
+
return { token, source: LEGACY_CLI_YML, notes: [] };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/vcs.ts
|
|
284
|
+
var SHORTHAND_HOSTS = {
|
|
285
|
+
github: "github.com",
|
|
286
|
+
gitlab: "gitlab.com",
|
|
287
|
+
bitbucket: "bitbucket.org"
|
|
288
|
+
};
|
|
289
|
+
var PACKAGE_JSON = "package.json repository";
|
|
290
|
+
var GIT_REMOTE = "git remote origin";
|
|
291
|
+
function parseRepositoryUrl(input) {
|
|
292
|
+
const s = input.trim();
|
|
293
|
+
let host;
|
|
294
|
+
let path;
|
|
295
|
+
const shorthand = /^(github|gitlab|bitbucket):(.+)$/.exec(s);
|
|
296
|
+
const scp = /^[^@/\s]+@([^:/\s]+):(.+)$/.exec(s);
|
|
297
|
+
if (shorthand) {
|
|
298
|
+
host = SHORTHAND_HOSTS[shorthand[1]];
|
|
299
|
+
path = shorthand[2];
|
|
300
|
+
} else if (/^[^/:\s]+\/[^/:\s]+$/.test(s)) {
|
|
301
|
+
host = "github.com";
|
|
302
|
+
path = s;
|
|
303
|
+
} else if (scp) {
|
|
304
|
+
host = scp[1];
|
|
305
|
+
path = scp[2];
|
|
306
|
+
} else {
|
|
307
|
+
let url;
|
|
308
|
+
try {
|
|
309
|
+
url = new URL(s.replace(/^git\+/, ""));
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol) || !url.hostname)
|
|
314
|
+
return null;
|
|
315
|
+
host = url.hostname;
|
|
316
|
+
path = url.pathname;
|
|
317
|
+
}
|
|
318
|
+
const parts = path.replace(/#.*$/, "").replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/");
|
|
319
|
+
if (parts.length < 2 || parts.some((p) => !p))
|
|
320
|
+
return null;
|
|
321
|
+
return `${host}/${parts.join("/")}`;
|
|
322
|
+
}
|
|
323
|
+
function repositoryFromPackageJson(repository) {
|
|
324
|
+
if (typeof repository === "string")
|
|
325
|
+
return repository;
|
|
326
|
+
if (repository && typeof repository === "object" && "url" in repository) {
|
|
327
|
+
const url = repository.url;
|
|
328
|
+
if (typeof url === "string")
|
|
329
|
+
return url;
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
function resolveVcsOrigin(input) {
|
|
334
|
+
if (input.flag)
|
|
335
|
+
return { value: input.flag, source: "--vcs-origin", warnings: [] };
|
|
336
|
+
const warnings = [];
|
|
337
|
+
const fromGit = input.gitRemote ? parseRepositoryUrl(input.gitRemote) : null;
|
|
338
|
+
if (input.pkgRepository !== undefined) {
|
|
339
|
+
const fromPkg = parseRepositoryUrl(input.pkgRepository);
|
|
340
|
+
if (fromPkg) {
|
|
341
|
+
if (fromGit && fromGit !== fromPkg) {
|
|
342
|
+
warnings.push(`${PACKAGE_JSON} (${fromPkg}) differs from ${GIT_REMOTE} (${fromGit}); using ${fromPkg}`);
|
|
343
|
+
}
|
|
344
|
+
return { value: fromPkg, source: PACKAGE_JSON, warnings };
|
|
345
|
+
}
|
|
346
|
+
warnings.push(`could not read a repository from ${PACKAGE_JSON} ("${input.pkgRepository}")`);
|
|
347
|
+
}
|
|
348
|
+
if (fromGit)
|
|
349
|
+
return { value: fromGit, source: GIT_REMOTE, warnings };
|
|
350
|
+
return { value: undefined, warnings };
|
|
351
|
+
}
|
|
352
|
+
|
|
229
353
|
// src/resolve.ts
|
|
230
354
|
var defaultResolveDeps = {
|
|
231
355
|
resolveOrgAndProjectId,
|
|
232
356
|
resolvePipelineDefinitionId,
|
|
233
|
-
log: (message) => console.error(message)
|
|
357
|
+
log: (message) => console.error(message),
|
|
358
|
+
env: process.env,
|
|
359
|
+
homedir: homedir(),
|
|
360
|
+
readFile: (path) => {
|
|
361
|
+
try {
|
|
362
|
+
return readFileSync(path, "utf8");
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
cwd: process.cwd(),
|
|
368
|
+
gitRemoteUrl: () => {
|
|
369
|
+
const r = run("git", ["remote", "get-url", "origin"]);
|
|
370
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
371
|
+
}
|
|
234
372
|
};
|
|
373
|
+
function resolveMissingVcsOrigin(opts, deps) {
|
|
374
|
+
if (opts.vcsOrigin)
|
|
375
|
+
return opts.vcsOrigin;
|
|
376
|
+
let pkgRepository;
|
|
377
|
+
const pkgText = deps.readFile(join2(deps.cwd, "package.json"));
|
|
378
|
+
if (pkgText !== null) {
|
|
379
|
+
try {
|
|
380
|
+
pkgRepository = repositoryFromPackageJson(JSON.parse(pkgText).repository);
|
|
381
|
+
} catch {
|
|
382
|
+
deps.log("warning: could not parse package.json while looking for a repository");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const found = resolveVcsOrigin({ pkgRepository, gitRemote: deps.gitRemoteUrl() });
|
|
386
|
+
for (const w of found.warnings)
|
|
387
|
+
deps.log(`warning: ${w}`);
|
|
388
|
+
if (found.value === undefined) {
|
|
389
|
+
throw new Error("--vcs-origin is required for --provider circleci and could not be inferred " + "(looked in: package.json repository, git remote origin). Pass e.g. --vcs-origin github.com/org/repo.");
|
|
390
|
+
}
|
|
391
|
+
deps.log(`vcs-origin: ${found.value} (from ${found.source})`);
|
|
392
|
+
opts.vcsOrigin = found.value;
|
|
393
|
+
return found.value;
|
|
394
|
+
}
|
|
235
395
|
async function resolveCircleciOptions(parsed, deps = defaultResolveDeps) {
|
|
236
396
|
const opts = parsed.options;
|
|
237
397
|
if (opts.provider !== "circleci")
|
|
238
398
|
return;
|
|
239
|
-
|
|
399
|
+
const vcsOrigin = resolveMissingVcsOrigin(opts, deps);
|
|
400
|
+
const missing = [
|
|
401
|
+
!opts.orgId && "--org-id",
|
|
402
|
+
!opts.projectId && "--project-id",
|
|
403
|
+
!opts.pipelineDefinitionId && "--pipeline-definition-id"
|
|
404
|
+
].filter((m) => Boolean(m));
|
|
405
|
+
if (missing.length === 0)
|
|
240
406
|
return;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
407
|
+
const found = resolveCircleToken({
|
|
408
|
+
flag: opts.circleToken,
|
|
409
|
+
env: deps.env,
|
|
410
|
+
homedir: deps.homedir,
|
|
411
|
+
readFile: deps.readFile
|
|
412
|
+
});
|
|
413
|
+
for (const note of found.notes)
|
|
414
|
+
deps.log(`note: ${note}`);
|
|
415
|
+
if (found.token === undefined) {
|
|
416
|
+
throw new Error(`missing ${missing.join(", ")} for --provider circleci. Pass them explicitly, ` + `or provide a CircleCI API token so they can be resolved (looked in: ${TOKEN_SOURCES.join(", ")}).`);
|
|
246
417
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
418
|
+
const { token, source } = found;
|
|
419
|
+
deps.log(`Using CircleCI token from ${source}`);
|
|
420
|
+
try {
|
|
421
|
+
if (!opts.orgId || !opts.projectId) {
|
|
422
|
+
deps.log("Resolving org-id and project-id via CircleCI API...");
|
|
423
|
+
const resolved = await deps.resolveOrgAndProjectId(token, vcsOrigin);
|
|
424
|
+
opts.orgId ??= resolved.orgId;
|
|
425
|
+
opts.projectId ??= resolved.projectId;
|
|
426
|
+
}
|
|
427
|
+
if (!opts.pipelineDefinitionId) {
|
|
428
|
+
deps.log("Resolving pipeline-definition-id via CircleCI API...");
|
|
429
|
+
opts.pipelineDefinitionId = await deps.resolvePipelineDefinitionId(token, opts.projectId, opts.file);
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
if (err instanceof CircleApiError && (err.status === 401 || err.status === 403)) {
|
|
433
|
+
throw new Error(`CircleCI rejected the token from ${source} (${err.status}). ` + `Pass a valid token with --circle-token or the CIRCLE_TOKEN environment variable.`);
|
|
434
|
+
}
|
|
435
|
+
throw err;
|
|
250
436
|
}
|
|
251
437
|
}
|
|
252
438
|
|
|
@@ -337,7 +523,7 @@ function displayCommand(args) {
|
|
|
337
523
|
}
|
|
338
524
|
|
|
339
525
|
// src/workspace.ts
|
|
340
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
526
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
341
527
|
import path2 from "node:path";
|
|
342
528
|
|
|
343
529
|
// src/glob.ts
|
|
@@ -480,7 +666,7 @@ function parsePnpmPackages(content) {
|
|
|
480
666
|
return result;
|
|
481
667
|
}
|
|
482
668
|
function readJson(file) {
|
|
483
|
-
return JSON.parse(
|
|
669
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
484
670
|
}
|
|
485
671
|
function readJsonSafe(file) {
|
|
486
672
|
try {
|
|
@@ -493,7 +679,7 @@ function readJsonSafe(file) {
|
|
|
493
679
|
function detectPatterns(root) {
|
|
494
680
|
const pnpmFile = path2.join(root, "pnpm-workspace.yaml");
|
|
495
681
|
if (existsSync(pnpmFile)) {
|
|
496
|
-
const patterns = parsePnpmPackages(
|
|
682
|
+
const patterns = parsePnpmPackages(readFileSync2(pnpmFile, "utf8"));
|
|
497
683
|
if (patterns.length > 0)
|
|
498
684
|
return { source: "pnpm-workspace.yaml", patterns };
|
|
499
685
|
}
|
|
@@ -564,7 +750,7 @@ function filterPackages(all, only, exclude) {
|
|
|
564
750
|
});
|
|
565
751
|
}
|
|
566
752
|
// package.json
|
|
567
|
-
var version = "0.
|
|
753
|
+
var version = "0.3.0";
|
|
568
754
|
|
|
569
755
|
// src/cli.ts
|
|
570
756
|
var VERSION = version;
|
|
@@ -578,11 +764,15 @@ Providers:
|
|
|
578
764
|
github --repo <owner/repo> --file <workflow> [--env <name>]
|
|
579
765
|
gitlab --project <group/project> --file <workflow> [--env <name>]
|
|
580
766
|
(--repo is accepted as an alias for --project)
|
|
581
|
-
circleci --vcs-origin <host/owner/repo> --circle-token <token> [--file <config>]
|
|
767
|
+
circleci [--vcs-origin <host/owner/repo>] [--circle-token <token>] [--file <config>]
|
|
582
768
|
[--org-id <uuid>] [--project-id <uuid>] [--pipeline-definition-id <uuid>]
|
|
583
769
|
[--context-id <uuid> ...]
|
|
584
|
-
|
|
585
|
-
|
|
770
|
+
--vcs-origin defaults to the root package.json "repository", then
|
|
771
|
+
\`git remote get-url origin\`.
|
|
772
|
+
Missing UUIDs are resolved via CircleCI API v2 using a token from
|
|
773
|
+
--circle-token, $CIRCLE_TOKEN, $CIRCLECI_TOKEN, $CIRCLE_CLI_TOKEN,
|
|
774
|
+
$CIRCLECI_CLI_TOKEN, or ~/.circleci/cli.yml (in that order).
|
|
775
|
+
Explicit UUIDs override auto-resolution; with all three, no token is needed.
|
|
586
776
|
|
|
587
777
|
Permissions (at least one required):
|
|
588
778
|
--allow-publish allow npm publish
|
|
@@ -603,8 +793,7 @@ as a single-package fallback. Private packages ("private": true) are always excl
|
|
|
603
793
|
Examples:
|
|
604
794
|
trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
|
|
605
795
|
trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
|
|
606
|
-
trustci --provider circleci --
|
|
607
|
-
--circle-token $CIRCLECI_TOKEN --allow-publish
|
|
796
|
+
CIRCLE_TOKEN=... trustci --provider circleci --allow-publish
|
|
608
797
|
trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
|
|
609
798
|
--pipeline-definition-id <uuid> --vcs-origin github.com/me/repo --allow-publish
|
|
610
799
|
trustci list
|
package/package.json
CHANGED