trustci 0.2.1 → 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 +28 -3
- package/dist/cli.js +311 -97
- 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
|
|
|
@@ -40,14 +40,39 @@ At least one of `--allow-publish` / `--allow-stage-publish` is required.
|
|
|
40
40
|
# GitLab
|
|
41
41
|
npx trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
|
|
42
42
|
|
|
43
|
-
# CircleCI (vcs-origin
|
|
43
|
+
# CircleCI (vcs-origin is host/owner/repo; a leading https:// is stripped automatically)
|
|
44
44
|
npx trustci --provider circleci \
|
|
45
45
|
--org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> \
|
|
46
|
-
--vcs-origin
|
|
46
|
+
--vcs-origin github.com/me/repo \
|
|
47
47
|
--context-id <uuid> \
|
|
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
|
@@ -7,6 +7,83 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
// src/args.ts
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
10
|
+
// src/circleci.ts
|
|
11
|
+
var BASE_URL = "https://circleci.com/api/v2";
|
|
12
|
+
function vcsTypeFromUrl(url) {
|
|
13
|
+
const { hostname } = new URL(url);
|
|
14
|
+
if (hostname === "github.com" || hostname.endsWith(".github.com"))
|
|
15
|
+
return "github";
|
|
16
|
+
if (hostname === "gitlab.com" || hostname.endsWith(".gitlab.com"))
|
|
17
|
+
return "gitlab";
|
|
18
|
+
if (hostname === "bitbucket.org" || hostname.endsWith(".bitbucket.org"))
|
|
19
|
+
return "bitbucket";
|
|
20
|
+
throw new Error(`Cannot determine VCS type from URL: ${url}`);
|
|
21
|
+
}
|
|
22
|
+
function normalizeVcsOrigin(value) {
|
|
23
|
+
const v = value.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/+$/, "");
|
|
24
|
+
const parts = v.split("/");
|
|
25
|
+
if (parts.length < 3 || parts.some((p) => !p)) {
|
|
26
|
+
throw new Error(`vcs-origin must be in the form host/owner/repo, e.g. github.com/org/repo (got "${value}").`);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
function projectSlugFromVcsOrigin(origin) {
|
|
31
|
+
const url = origin.includes("://") ? origin : `https://${origin}`;
|
|
32
|
+
const vcsType = vcsTypeFromUrl(url);
|
|
33
|
+
const { pathname } = new URL(url);
|
|
34
|
+
const parts = pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
|
|
35
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
36
|
+
throw new Error(`Cannot extract org/repo from URL: ${url}`);
|
|
37
|
+
}
|
|
38
|
+
return `${vcsType}/${parts[0]}/${parts[1]}`;
|
|
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
|
+
}
|
|
52
|
+
async function circleGet(token, path) {
|
|
53
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
54
|
+
headers: { "Circle-Token": token, Accept: "application/json" }
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new CircleApiError(response.status, path, await response.text());
|
|
58
|
+
}
|
|
59
|
+
return response.json();
|
|
60
|
+
}
|
|
61
|
+
async function resolveOrgAndProjectId(token, vcsOrigin) {
|
|
62
|
+
const slug = projectSlugFromVcsOrigin(vcsOrigin);
|
|
63
|
+
const project = await circleGet(token, `/project/${slug}`);
|
|
64
|
+
return { orgId: project.organization_id, projectId: project.id };
|
|
65
|
+
}
|
|
66
|
+
async function resolvePipelineDefinitionId(token, projectId, file) {
|
|
67
|
+
const data = await circleGet(token, `/projects/${encodeURIComponent(projectId)}/pipeline-definitions`);
|
|
68
|
+
if (data.items.length === 0) {
|
|
69
|
+
throw new Error(`No pipeline definitions found for project "${projectId}".`);
|
|
70
|
+
}
|
|
71
|
+
if (file) {
|
|
72
|
+
const match = data.items.find((d) => d.config_source?.file_path === file || d.name === file);
|
|
73
|
+
if (!match) {
|
|
74
|
+
const available2 = data.items.map((d) => d.config_source?.file_path ?? d.name ?? d.id).join(", ");
|
|
75
|
+
throw new Error(`No pipeline definition found matching "${file}". Available: ${available2}`);
|
|
76
|
+
}
|
|
77
|
+
return match.id;
|
|
78
|
+
}
|
|
79
|
+
const [only] = data.items;
|
|
80
|
+
if (data.items.length === 1 && only) {
|
|
81
|
+
return only.id;
|
|
82
|
+
}
|
|
83
|
+
const available = data.items.map((d) => d.config_source?.file_path ?? d.name ?? d.id).join(", ");
|
|
84
|
+
throw new Error(`Multiple pipeline definitions found for project "${projectId}". Specify --file to select one: ${available}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
10
87
|
// src/types.ts
|
|
11
88
|
var PROVIDERS = ["github", "gitlab", "circleci"];
|
|
12
89
|
|
|
@@ -102,22 +179,17 @@ function parseCliArgs(argv) {
|
|
|
102
179
|
break;
|
|
103
180
|
}
|
|
104
181
|
case "circleci": {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
182
|
+
const rawVcsOrigin = values["vcs-origin"];
|
|
183
|
+
let vcsOrigin;
|
|
184
|
+
try {
|
|
185
|
+
vcsOrigin = rawVcsOrigin === undefined ? undefined : normalizeVcsOrigin(rawVcsOrigin);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw new ArgError(`--${e.message}`);
|
|
108
188
|
}
|
|
109
189
|
const circleToken = values["circle-token"];
|
|
110
190
|
const orgId = values["org-id"];
|
|
111
191
|
const projectId = values["project-id"];
|
|
112
192
|
const pipelineDefinitionId = values["pipeline-definition-id"];
|
|
113
|
-
if (!circleToken) {
|
|
114
|
-
if (!orgId)
|
|
115
|
-
throw new ArgError("--org-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
116
|
-
if (!projectId)
|
|
117
|
-
throw new ArgError("--project-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
118
|
-
if (!pipelineDefinitionId)
|
|
119
|
-
throw new ArgError("--pipeline-definition-id is required for --provider circleci (or provide --circle-token for auto-resolution).");
|
|
120
|
-
}
|
|
121
193
|
providerOptions = {
|
|
122
194
|
provider: "circleci",
|
|
123
195
|
circleToken,
|
|
@@ -141,63 +213,6 @@ function parseCliArgs(argv) {
|
|
|
141
213
|
};
|
|
142
214
|
}
|
|
143
215
|
|
|
144
|
-
// src/circleci.ts
|
|
145
|
-
var BASE_URL = "https://circleci.com/api/v2";
|
|
146
|
-
function vcsTypeFromUrl(url) {
|
|
147
|
-
const { hostname } = new URL(url);
|
|
148
|
-
if (hostname === "github.com" || hostname.endsWith(".github.com"))
|
|
149
|
-
return "github";
|
|
150
|
-
if (hostname === "gitlab.com" || hostname.endsWith(".gitlab.com"))
|
|
151
|
-
return "gitlab";
|
|
152
|
-
if (hostname === "bitbucket.org" || hostname.endsWith(".bitbucket.org"))
|
|
153
|
-
return "bitbucket";
|
|
154
|
-
throw new Error(`Cannot determine VCS type from URL: ${url}`);
|
|
155
|
-
}
|
|
156
|
-
function projectSlugFromVcsOrigin(url) {
|
|
157
|
-
const vcsType = vcsTypeFromUrl(url);
|
|
158
|
-
const { pathname } = new URL(url);
|
|
159
|
-
const parts = pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
|
|
160
|
-
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
161
|
-
throw new Error(`Cannot extract org/repo from URL: ${url}`);
|
|
162
|
-
}
|
|
163
|
-
return `${vcsType}/${parts[0]}/${parts[1]}`;
|
|
164
|
-
}
|
|
165
|
-
async function circleGet(token, path) {
|
|
166
|
-
const response = await fetch(`${BASE_URL}${path}`, {
|
|
167
|
-
headers: { "Circle-Token": token, Accept: "application/json" }
|
|
168
|
-
});
|
|
169
|
-
if (!response.ok) {
|
|
170
|
-
const body = await response.text();
|
|
171
|
-
throw new Error(`CircleCI API error (${response.status}) for ${path}: ${body}`);
|
|
172
|
-
}
|
|
173
|
-
return response.json();
|
|
174
|
-
}
|
|
175
|
-
async function resolveOrgAndProjectId(token, vcsOrigin) {
|
|
176
|
-
const slug = projectSlugFromVcsOrigin(vcsOrigin);
|
|
177
|
-
const project = await circleGet(token, `/project/${slug}`);
|
|
178
|
-
return { orgId: project.organization_id, projectId: project.id };
|
|
179
|
-
}
|
|
180
|
-
async function resolvePipelineDefinitionId(token, projectId, file) {
|
|
181
|
-
const data = await circleGet(token, `/pipeline-definitions?project_id=${encodeURIComponent(projectId)}`);
|
|
182
|
-
if (data.items.length === 0) {
|
|
183
|
-
throw new Error(`No pipeline definitions found for project "${projectId}".`);
|
|
184
|
-
}
|
|
185
|
-
if (file) {
|
|
186
|
-
const match = data.items.find((d) => d.config_file_path === file || d.name === file);
|
|
187
|
-
if (!match) {
|
|
188
|
-
const available2 = data.items.map((d) => d.config_file_path ?? d.name ?? d.id).join(", ");
|
|
189
|
-
throw new Error(`No pipeline definition found matching "${file}". Available: ${available2}`);
|
|
190
|
-
}
|
|
191
|
-
return match.id;
|
|
192
|
-
}
|
|
193
|
-
const [only] = data.items;
|
|
194
|
-
if (data.items.length === 1 && only) {
|
|
195
|
-
return only.id;
|
|
196
|
-
}
|
|
197
|
-
const available = data.items.map((d) => d.config_file_path ?? d.name ?? d.id).join(", ");
|
|
198
|
-
throw new Error(`Multiple pipeline definitions found for project "${projectId}". Specify --file to select one: ${available}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
216
|
// src/exec.ts
|
|
202
217
|
import { spawnSync } from "node:child_process";
|
|
203
218
|
function run(cmd, args) {
|
|
@@ -213,6 +228,214 @@ function runInherit(cmd, args) {
|
|
|
213
228
|
return res.status ?? (res.error || res.signal ? 1 : 0);
|
|
214
229
|
}
|
|
215
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
|
+
|
|
353
|
+
// src/resolve.ts
|
|
354
|
+
var defaultResolveDeps = {
|
|
355
|
+
resolveOrgAndProjectId,
|
|
356
|
+
resolvePipelineDefinitionId,
|
|
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
|
+
}
|
|
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
|
+
}
|
|
395
|
+
async function resolveCircleciOptions(parsed, deps = defaultResolveDeps) {
|
|
396
|
+
const opts = parsed.options;
|
|
397
|
+
if (opts.provider !== "circleci")
|
|
398
|
+
return;
|
|
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)
|
|
406
|
+
return;
|
|
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(", ")}).`);
|
|
417
|
+
}
|
|
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;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
216
439
|
// src/npm.ts
|
|
217
440
|
var MIN_NODE = "22.14.0";
|
|
218
441
|
var MIN_NPM = "11.5.1";
|
|
@@ -300,7 +523,7 @@ function displayCommand(args) {
|
|
|
300
523
|
}
|
|
301
524
|
|
|
302
525
|
// src/workspace.ts
|
|
303
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
526
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
304
527
|
import path2 from "node:path";
|
|
305
528
|
|
|
306
529
|
// src/glob.ts
|
|
@@ -443,7 +666,7 @@ function parsePnpmPackages(content) {
|
|
|
443
666
|
return result;
|
|
444
667
|
}
|
|
445
668
|
function readJson(file) {
|
|
446
|
-
return JSON.parse(
|
|
669
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
447
670
|
}
|
|
448
671
|
function readJsonSafe(file) {
|
|
449
672
|
try {
|
|
@@ -456,7 +679,7 @@ function readJsonSafe(file) {
|
|
|
456
679
|
function detectPatterns(root) {
|
|
457
680
|
const pnpmFile = path2.join(root, "pnpm-workspace.yaml");
|
|
458
681
|
if (existsSync(pnpmFile)) {
|
|
459
|
-
const patterns = parsePnpmPackages(
|
|
682
|
+
const patterns = parsePnpmPackages(readFileSync2(pnpmFile, "utf8"));
|
|
460
683
|
if (patterns.length > 0)
|
|
461
684
|
return { source: "pnpm-workspace.yaml", patterns };
|
|
462
685
|
}
|
|
@@ -526,9 +749,11 @@ function filterPackages(all, only, exclude) {
|
|
|
526
749
|
return true;
|
|
527
750
|
});
|
|
528
751
|
}
|
|
752
|
+
// package.json
|
|
753
|
+
var version = "0.3.0";
|
|
529
754
|
|
|
530
755
|
// src/cli.ts
|
|
531
|
-
var VERSION =
|
|
756
|
+
var VERSION = version;
|
|
532
757
|
var HELP = `trustci — bulk-register npm Trusted Publishers across a monorepo
|
|
533
758
|
|
|
534
759
|
Usage:
|
|
@@ -539,11 +764,15 @@ Providers:
|
|
|
539
764
|
github --repo <owner/repo> --file <workflow> [--env <name>]
|
|
540
765
|
gitlab --project <group/project> --file <workflow> [--env <name>]
|
|
541
766
|
(--repo is accepted as an alias for --project)
|
|
542
|
-
circleci --vcs-origin <
|
|
767
|
+
circleci [--vcs-origin <host/owner/repo>] [--circle-token <token>] [--file <config>]
|
|
543
768
|
[--org-id <uuid>] [--project-id <uuid>] [--pipeline-definition-id <uuid>]
|
|
544
769
|
[--context-id <uuid> ...]
|
|
545
|
-
|
|
546
|
-
|
|
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.
|
|
547
776
|
|
|
548
777
|
Permissions (at least one required):
|
|
549
778
|
--allow-publish allow npm publish
|
|
@@ -564,10 +793,9 @@ as a single-package fallback. Private packages ("private": true) are always excl
|
|
|
564
793
|
Examples:
|
|
565
794
|
trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
|
|
566
795
|
trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
|
|
567
|
-
trustci --provider circleci --
|
|
568
|
-
--circle-token $CIRCLECI_TOKEN --allow-publish
|
|
796
|
+
CIRCLE_TOKEN=... trustci --provider circleci --allow-publish
|
|
569
797
|
trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
|
|
570
|
-
--pipeline-definition-id <uuid> --vcs-origin
|
|
798
|
+
--pipeline-definition-id <uuid> --vcs-origin github.com/me/repo --allow-publish
|
|
571
799
|
trustci list
|
|
572
800
|
`;
|
|
573
801
|
async function confirm(question) {
|
|
@@ -712,25 +940,11 @@ async function main() {
|
|
|
712
940
|
}
|
|
713
941
|
if (parsed.kind === "list")
|
|
714
942
|
return runList(parsed.pkg, parsed.dryRun);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
console.error("Resolving org-id and project-id via CircleCI API...");
|
|
721
|
-
const resolved = await resolveOrgAndProjectId(opts.circleToken, opts.vcsOrigin);
|
|
722
|
-
opts.orgId ??= resolved.orgId;
|
|
723
|
-
opts.projectId ??= resolved.projectId;
|
|
724
|
-
}
|
|
725
|
-
if (!opts.pipelineDefinitionId) {
|
|
726
|
-
console.error("Resolving pipeline-definition-id via CircleCI API...");
|
|
727
|
-
opts.pipelineDefinitionId = await resolvePipelineDefinitionId(opts.circleToken, opts.projectId, opts.file);
|
|
728
|
-
}
|
|
729
|
-
} catch (err) {
|
|
730
|
-
console.error(`error: ${err.message}`);
|
|
731
|
-
return 1;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
943
|
+
try {
|
|
944
|
+
await resolveCircleciOptions(parsed);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
console.error(`error: ${err.message}`);
|
|
947
|
+
return 1;
|
|
734
948
|
}
|
|
735
949
|
return runTrust(parsed);
|
|
736
950
|
}
|
package/package.json
CHANGED