trustci 0.2.2 → 0.3.1
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 +70 -3
- package/dist/cli.js +244 -35
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Setting up trusted publishing one package at a time is tedious in a monorepo.
|
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
- **npm** 11.
|
|
13
|
+
- **npm** 11.15.0 or later (for `npm trust` with `--allow-publish` / `--allow-stage-publish`)
|
|
14
14
|
- **Node** 22.14.0 or later
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
@@ -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` `--vcs-origin <host/owner/repo>`
|
|
35
|
+
| `circleci` | A CircleCI token, or all of `--org-id` `--project-id` `--pipeline-definition-id` | `--vcs-origin <host/owner/repo>` (inferred), `--circle-token`, `--file`, `--context-id` (repeatable) |
|
|
36
36
|
|
|
37
37
|
At least one of `--allow-publish` / `--allow-stage-publish` is required.
|
|
38
38
|
|
|
@@ -40,7 +40,12 @@ 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
|
|
43
|
+
# CircleCI — minimal form: UUIDs are resolved via the API and --vcs-origin
|
|
44
|
+
# is inferred, so a token (here via the environment) is all you need to add
|
|
45
|
+
CIRCLE_TOKEN=... npx trustci --provider circleci --allow-publish
|
|
46
|
+
|
|
47
|
+
# CircleCI — fully explicit form
|
|
48
|
+
# (vcs-origin is host/owner/repo; a leading https:// is stripped automatically)
|
|
44
49
|
npx trustci --provider circleci \
|
|
45
50
|
--org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> \
|
|
46
51
|
--vcs-origin github.com/me/repo \
|
|
@@ -48,6 +53,47 @@ npx trustci --provider circleci \
|
|
|
48
53
|
--allow-publish
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
#### CircleCI `--vcs-origin`
|
|
57
|
+
|
|
58
|
+
When `--vcs-origin` is omitted, trustci takes it from the root `package.json`
|
|
59
|
+
`repository` field (URL, `git+https://…`, `git@host:owner/repo`, or the
|
|
60
|
+
`github:` / `gitlab:` / `bitbucket:` / `owner/repo` shorthands), then from
|
|
61
|
+
`git remote get-url origin`. It prints which one it used, and warns when the two
|
|
62
|
+
disagree (`package.json` wins, since it is committed rather than clone-specific).
|
|
63
|
+
|
|
64
|
+
#### CircleCI token lookup
|
|
65
|
+
|
|
66
|
+
When any of the three UUIDs is missing, trustci resolves it through the CircleCI
|
|
67
|
+
API v2. The token is taken from the first of these that is set, and trustci
|
|
68
|
+
prints which one it used (never the token itself):
|
|
69
|
+
|
|
70
|
+
1. `--circle-token`
|
|
71
|
+
2. `CIRCLE_TOKEN` (circleci CLI v1, chunk)
|
|
72
|
+
3. `CIRCLECI_TOKEN` (chunk)
|
|
73
|
+
4. `CIRCLE_CLI_TOKEN` (circleci CLI v1, legacy alias)
|
|
74
|
+
5. `CIRCLECI_CLI_TOKEN` (circleci CLI v0.1)
|
|
75
|
+
6. `token` in `~/.circleci/cli.yml`, written by `circleci setup` in circleci CLI v0.1.
|
|
76
|
+
It is ignored when its `host` is not `https://circleci.com`.
|
|
77
|
+
|
|
78
|
+
The OS keychain, where circleci CLI v1 and chunk store tokens by default, is not read.
|
|
79
|
+
When all three UUIDs are given, no token is looked up.
|
|
80
|
+
|
|
81
|
+
#### CircleCI pipeline definitions and `--file`
|
|
82
|
+
|
|
83
|
+
A missing `--pipeline-definition-id` is resolved by listing the project's
|
|
84
|
+
pipeline definitions. When there is exactly one, it is used automatically.
|
|
85
|
+
When the project has several — e.g. a config sourced from another repository
|
|
86
|
+
alongside `.circleci/config.yml` — pass `--file` with either the definition's
|
|
87
|
+
config file path or its name; the error message lists the candidates:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx trustci --provider circleci --allow-publish \
|
|
91
|
+
--file workflows/publish/npm-polyrepo-bun-semantic-release.yaml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`--file` is only used for this lookup — it is not passed to `npm trust`, and
|
|
95
|
+
it has no effect when `--pipeline-definition-id` is given explicitly.
|
|
96
|
+
|
|
51
97
|
### Common options
|
|
52
98
|
|
|
53
99
|
| Flag | Description |
|
|
@@ -70,6 +116,18 @@ Patterns are read from the first of:
|
|
|
70
116
|
Packages marked `"private": true` are always excluded. On a real (non-dry) run,
|
|
71
117
|
packages not yet published to the registry are skipped with a warning.
|
|
72
118
|
|
|
119
|
+
## Agent skill
|
|
120
|
+
|
|
121
|
+
The repository ships an [Agent Skill](https://agentskills.io/) at
|
|
122
|
+
[`skills/trustci/SKILL.md`](https://github.com/hideokamoto/trustci/blob/main/skills/trustci/SKILL.md)
|
|
123
|
+
that teaches coding agents (Claude Code, Codex, Cursor, …) to run trustci safely:
|
|
124
|
+
dry-run first, then a real run only after you confirm. Install it with the
|
|
125
|
+
[`skills`](https://github.com/vercel-labs/skills) CLI:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx skills add hideokamoto/trustci
|
|
129
|
+
```
|
|
130
|
+
|
|
73
131
|
## Development
|
|
74
132
|
|
|
75
133
|
Built with [Bun](https://bun.sh), ships zero runtime dependencies, and the
|
|
@@ -82,6 +140,15 @@ bun run typecheck
|
|
|
82
140
|
bun run build # -> dist/cli.js (npx entry point), via `bun build --target node`
|
|
83
141
|
```
|
|
84
142
|
|
|
143
|
+
The documentation site lives in `docs/` ([Astro Starlight](https://starlight.astro.build/)):
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
cd docs
|
|
147
|
+
bun install
|
|
148
|
+
bun run dev # local preview
|
|
149
|
+
bun run build # -> docs/dist
|
|
150
|
+
```
|
|
151
|
+
|
|
85
152
|
`bun install` also enables a husky `commit-msg` hook that runs
|
|
86
153
|
[commitlint](https://commitlint.js.org/) with `@commitlint/config-conventional`,
|
|
87
154
|
so every commit must be a [Conventional Commit](https://www.conventionalcommits.org/).
|
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,33 +228,217 @@ 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
|
|
|
253
439
|
// src/npm.ts
|
|
254
440
|
var MIN_NODE = "22.14.0";
|
|
255
|
-
var MIN_NPM = "11.
|
|
441
|
+
var MIN_NPM = "11.15.0";
|
|
256
442
|
function cmpVersion(a, b) {
|
|
257
443
|
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
258
444
|
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -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
|
}
|
|
@@ -537,6 +723,9 @@ function discoverPackages(root) {
|
|
|
537
723
|
const dirs = expandPatterns(root, patterns);
|
|
538
724
|
const all = [];
|
|
539
725
|
const seen = new Set;
|
|
726
|
+
if (existsSync(path2.join(root, "pnpm-workspace.yaml"))) {
|
|
727
|
+
dirs.unshift(path2.resolve(root));
|
|
728
|
+
}
|
|
540
729
|
for (const dir of dirs) {
|
|
541
730
|
const pkg = readPkg(dir);
|
|
542
731
|
if (pkg && !seen.has(pkg.name)) {
|
|
@@ -564,7 +753,7 @@ function filterPackages(all, only, exclude) {
|
|
|
564
753
|
});
|
|
565
754
|
}
|
|
566
755
|
// package.json
|
|
567
|
-
var version = "0.
|
|
756
|
+
var version = "0.3.1";
|
|
568
757
|
|
|
569
758
|
// src/cli.ts
|
|
570
759
|
var VERSION = version;
|
|
@@ -574,40 +763,57 @@ Usage:
|
|
|
574
763
|
trustci [trust] --provider <github|gitlab|circleci> [provider flags] [options]
|
|
575
764
|
trustci list [package] [--dry-run]
|
|
576
765
|
|
|
766
|
+
Commands:
|
|
767
|
+
trust register a trusted publisher for every public workspace package
|
|
768
|
+
(the default; "trust" may be omitted)
|
|
769
|
+
list run \`npm trust list\` for <package>, or for every public workspace package
|
|
770
|
+
|
|
577
771
|
Providers:
|
|
578
772
|
github --repo <owner/repo> --file <workflow> [--env <name>]
|
|
579
773
|
gitlab --project <group/project> --file <workflow> [--env <name>]
|
|
580
774
|
(--repo is accepted as an alias for --project)
|
|
581
|
-
circleci --vcs-origin <host/owner/repo> --circle-token <token> [--file <config>]
|
|
775
|
+
circleci [--vcs-origin <host/owner/repo>] [--circle-token <token>] [--file <config>]
|
|
582
776
|
[--org-id <uuid>] [--project-id <uuid>] [--pipeline-definition-id <uuid>]
|
|
583
777
|
[--context-id <uuid> ...]
|
|
584
|
-
|
|
585
|
-
|
|
778
|
+
--vcs-origin defaults to the root package.json "repository", then
|
|
779
|
+
\`git remote get-url origin\`.
|
|
780
|
+
Missing UUIDs are resolved via CircleCI API v2 (also under --dry-run)
|
|
781
|
+
using a token from --circle-token, $CIRCLE_TOKEN, $CIRCLECI_TOKEN,
|
|
782
|
+
$CIRCLE_CLI_TOKEN, $CIRCLECI_CLI_TOKEN, or ~/.circleci/cli.yml (in that order).
|
|
783
|
+
Explicit UUIDs override auto-resolution; with all three, no token is needed.
|
|
784
|
+
--file selects the pipeline definition by config path or name (needed
|
|
785
|
+
when the project has more than one); it is not passed to npm trust.
|
|
586
786
|
|
|
587
787
|
Permissions (at least one required):
|
|
588
788
|
--allow-publish allow npm publish
|
|
589
789
|
--allow-stage-publish allow npm stage publish
|
|
590
790
|
|
|
591
791
|
Options:
|
|
592
|
-
--dry-run print the npm
|
|
792
|
+
--dry-run print the npm commands without executing them
|
|
593
793
|
--only <name-glob> only include packages matching (repeatable)
|
|
594
794
|
--exclude <name-glob> exclude packages matching (repeatable)
|
|
595
|
-
|
|
795
|
+
globs match the whole name; * also matches "/"
|
|
796
|
+
-y, --yes skip the confirmation prompt (and npm's);
|
|
797
|
+
required when stdin is not a terminal
|
|
596
798
|
-h, --help show this help
|
|
597
799
|
-v, --version show the trustci version
|
|
598
800
|
|
|
599
801
|
Workspaces are detected from pnpm-workspace.yaml, package.json "workspaces",
|
|
600
802
|
or lerna.json. If no workspace config is found, the root package.json is used
|
|
601
803
|
as a single-package fallback. Private packages ("private": true) are always excluded.
|
|
804
|
+
On a real run, packages not yet published to the registry are skipped.
|
|
602
805
|
|
|
603
806
|
Examples:
|
|
604
807
|
trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
|
|
605
808
|
trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
|
|
606
|
-
trustci --provider circleci --
|
|
607
|
-
|
|
809
|
+
CIRCLE_TOKEN=... trustci --provider circleci --allow-publish
|
|
810
|
+
trustci --provider circleci --file .circleci/config.yml --allow-publish
|
|
608
811
|
trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
|
|
609
812
|
--pipeline-definition-id <uuid> --vcs-origin github.com/me/repo --allow-publish
|
|
610
813
|
trustci list
|
|
814
|
+
|
|
815
|
+
Requires npm >= 11.15.0 and Node >= 22.14.0.
|
|
816
|
+
Documentation: https://trustci.hidetaka.dev
|
|
611
817
|
`;
|
|
612
818
|
async function confirm(question) {
|
|
613
819
|
const rl = createInterface({ input: process2.stdin, output: process2.stderr });
|
|
@@ -655,6 +861,9 @@ async function runTrust(parsed) {
|
|
|
655
861
|
const candidates = filterPackages(all, parsed.only, parsed.exclude);
|
|
656
862
|
if (candidates.length === 0) {
|
|
657
863
|
console.error(`No public packages matched (out of ${all.length} discovered via ${source}).`);
|
|
864
|
+
for (const pkg of all) {
|
|
865
|
+
console.error(` - ${pkg.name}${pkg.private ? " (private)" : ""}`);
|
|
866
|
+
}
|
|
658
867
|
return 1;
|
|
659
868
|
}
|
|
660
869
|
console.error(`Detected ${all.length} package(s) via ${source}; ${candidates.length} public after filters.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trustci",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Bulk-register npm Trusted Publishers (npm trust) across all public packages in a monorepo. Supports GitHub, GitLab and CircleCI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=22.14.0",
|
|
14
|
-
"npm": ">=11.
|
|
14
|
+
"npm": ">=11.15.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "bun build src/cli.ts --target node --outfile dist/cli.js",
|