trustci 0.2.0 → 0.2.2
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 +108 -82
- package/package.json +19 -2
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` `--vcs-origin <
|
|
35
|
+
| `circleci` | `--org-id` `--project-id` `--pipeline-definition-id` `--vcs-origin <host/owner/repo>` | `--context-id` (repeatable) |
|
|
36
36
|
|
|
37
37
|
At least one of `--allow-publish` / `--allow-stage-publish` is required.
|
|
38
38
|
|
|
@@ -40,10 +40,10 @@ 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
|
```
|
|
@@ -78,9 +78,34 @@ published artifact is plain Node-compatible JS.
|
|
|
78
78
|
```bash
|
|
79
79
|
bun install
|
|
80
80
|
bun test
|
|
81
|
+
bun run typecheck
|
|
81
82
|
bun run build # -> dist/cli.js (npx entry point), via `bun build --target node`
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
`bun install` also enables a husky `commit-msg` hook that runs
|
|
86
|
+
[commitlint](https://commitlint.js.org/) with `@commitlint/config-conventional`,
|
|
87
|
+
so every commit must be a [Conventional Commit](https://www.conventionalcommits.org/).
|
|
88
|
+
|
|
89
|
+
## Releasing
|
|
90
|
+
|
|
91
|
+
Releases are automated: every merge to `main` runs the CircleCI config
|
|
92
|
+
`workflows/publish/npm-polyrepo-bun-semantic-release.yaml` from
|
|
93
|
+
[hideokamoto/circleci-configurations](https://github.com/hideokamoto/circleci-configurations).
|
|
94
|
+
[semantic-release](https://github.com/semantic-release/semantic-release) decides the
|
|
95
|
+
next version from the commits since the latest `v*` tag, pushes the tag, publishes
|
|
96
|
+
to npm via Trusted Publishing (OIDC), and creates the GitHub Release.
|
|
97
|
+
|
|
98
|
+
| Commit | Release |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `fix:` / `perf:` | patch |
|
|
101
|
+
| `feat:` | minor |
|
|
102
|
+
| `feat!:` / `fix!:` or a `BREAKING CHANGE:` footer | major |
|
|
103
|
+
| `chore:` / `docs:` / `ci:` / `test:` / `refactor:` … | none |
|
|
104
|
+
|
|
105
|
+
Do not bump `version` in `package.json`, create tags, or run `npm publish` manually.
|
|
106
|
+
Release notes are published on
|
|
107
|
+
[GitHub Releases](https://github.com/hideokamoto/trustci/releases).
|
|
108
|
+
|
|
84
109
|
## License
|
|
85
110
|
|
|
86
111
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,72 @@ 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
|
+
async function circleGet(token, path) {
|
|
41
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
42
|
+
headers: { "Circle-Token": token, Accept: "application/json" }
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
throw new Error(`CircleCI API error (${response.status}) for ${path}: ${body}`);
|
|
47
|
+
}
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
async function resolveOrgAndProjectId(token, vcsOrigin) {
|
|
51
|
+
const slug = projectSlugFromVcsOrigin(vcsOrigin);
|
|
52
|
+
const project = await circleGet(token, `/project/${slug}`);
|
|
53
|
+
return { orgId: project.organization_id, projectId: project.id };
|
|
54
|
+
}
|
|
55
|
+
async function resolvePipelineDefinitionId(token, projectId, file) {
|
|
56
|
+
const data = await circleGet(token, `/projects/${encodeURIComponent(projectId)}/pipeline-definitions`);
|
|
57
|
+
if (data.items.length === 0) {
|
|
58
|
+
throw new Error(`No pipeline definitions found for project "${projectId}".`);
|
|
59
|
+
}
|
|
60
|
+
if (file) {
|
|
61
|
+
const match = data.items.find((d) => d.config_source?.file_path === file || d.name === file);
|
|
62
|
+
if (!match) {
|
|
63
|
+
const available2 = data.items.map((d) => d.config_source?.file_path ?? d.name ?? d.id).join(", ");
|
|
64
|
+
throw new Error(`No pipeline definition found matching "${file}". Available: ${available2}`);
|
|
65
|
+
}
|
|
66
|
+
return match.id;
|
|
67
|
+
}
|
|
68
|
+
const [only] = data.items;
|
|
69
|
+
if (data.items.length === 1 && only) {
|
|
70
|
+
return only.id;
|
|
71
|
+
}
|
|
72
|
+
const available = data.items.map((d) => d.config_source?.file_path ?? d.name ?? d.id).join(", ");
|
|
73
|
+
throw new Error(`Multiple pipeline definitions found for project "${projectId}". Specify --file to select one: ${available}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
10
76
|
// src/types.ts
|
|
11
77
|
var PROVIDERS = ["github", "gitlab", "circleci"];
|
|
12
78
|
|
|
@@ -102,9 +168,13 @@ function parseCliArgs(argv) {
|
|
|
102
168
|
break;
|
|
103
169
|
}
|
|
104
170
|
case "circleci": {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
171
|
+
let vcsOrigin;
|
|
172
|
+
try {
|
|
173
|
+
vcsOrigin = normalizeVcsOrigin(require_(values["vcs-origin"], "vcs-origin", "circleci"));
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e instanceof ArgError)
|
|
176
|
+
throw e;
|
|
177
|
+
throw new ArgError(`--${e.message}`);
|
|
108
178
|
}
|
|
109
179
|
const circleToken = values["circle-token"];
|
|
110
180
|
const orgId = values["org-id"];
|
|
@@ -141,62 +211,6 @@ function parseCliArgs(argv) {
|
|
|
141
211
|
};
|
|
142
212
|
}
|
|
143
213
|
|
|
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
|
-
if (data.items.length === 1) {
|
|
194
|
-
return data.items[0].id;
|
|
195
|
-
}
|
|
196
|
-
const available = data.items.map((d) => d.config_file_path ?? d.name ?? d.id).join(", ");
|
|
197
|
-
throw new Error(`Multiple pipeline definitions found for project "${projectId}". Specify --file to select one: ${available}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
214
|
// src/exec.ts
|
|
201
215
|
import { spawnSync } from "node:child_process";
|
|
202
216
|
function run(cmd, args) {
|
|
@@ -212,6 +226,30 @@ function runInherit(cmd, args) {
|
|
|
212
226
|
return res.status ?? (res.error || res.signal ? 1 : 0);
|
|
213
227
|
}
|
|
214
228
|
|
|
229
|
+
// src/resolve.ts
|
|
230
|
+
var defaultResolveDeps = {
|
|
231
|
+
resolveOrgAndProjectId,
|
|
232
|
+
resolvePipelineDefinitionId,
|
|
233
|
+
log: (message) => console.error(message)
|
|
234
|
+
};
|
|
235
|
+
async function resolveCircleciOptions(parsed, deps = defaultResolveDeps) {
|
|
236
|
+
const opts = parsed.options;
|
|
237
|
+
if (opts.provider !== "circleci")
|
|
238
|
+
return;
|
|
239
|
+
if (!opts.circleToken)
|
|
240
|
+
return;
|
|
241
|
+
if (!opts.orgId || !opts.projectId) {
|
|
242
|
+
deps.log("Resolving org-id and project-id via CircleCI API...");
|
|
243
|
+
const resolved = await deps.resolveOrgAndProjectId(opts.circleToken, opts.vcsOrigin);
|
|
244
|
+
opts.orgId ??= resolved.orgId;
|
|
245
|
+
opts.projectId ??= resolved.projectId;
|
|
246
|
+
}
|
|
247
|
+
if (!opts.pipelineDefinitionId) {
|
|
248
|
+
deps.log("Resolving pipeline-definition-id via CircleCI API...");
|
|
249
|
+
opts.pipelineDefinitionId = await deps.resolvePipelineDefinitionId(opts.circleToken, opts.projectId, opts.file);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
215
253
|
// src/npm.ts
|
|
216
254
|
var MIN_NODE = "22.14.0";
|
|
217
255
|
var MIN_NPM = "11.5.1";
|
|
@@ -525,9 +563,11 @@ function filterPackages(all, only, exclude) {
|
|
|
525
563
|
return true;
|
|
526
564
|
});
|
|
527
565
|
}
|
|
566
|
+
// package.json
|
|
567
|
+
var version = "0.2.2";
|
|
528
568
|
|
|
529
569
|
// src/cli.ts
|
|
530
|
-
var VERSION =
|
|
570
|
+
var VERSION = version;
|
|
531
571
|
var HELP = `trustci — bulk-register npm Trusted Publishers across a monorepo
|
|
532
572
|
|
|
533
573
|
Usage:
|
|
@@ -538,7 +578,7 @@ Providers:
|
|
|
538
578
|
github --repo <owner/repo> --file <workflow> [--env <name>]
|
|
539
579
|
gitlab --project <group/project> --file <workflow> [--env <name>]
|
|
540
580
|
(--repo is accepted as an alias for --project)
|
|
541
|
-
circleci --vcs-origin <
|
|
581
|
+
circleci --vcs-origin <host/owner/repo> --circle-token <token> [--file <config>]
|
|
542
582
|
[--org-id <uuid>] [--project-id <uuid>] [--pipeline-definition-id <uuid>]
|
|
543
583
|
[--context-id <uuid> ...]
|
|
544
584
|
UUIDs are auto-resolved via CircleCI API v2 when --circle-token is provided.
|
|
@@ -563,10 +603,10 @@ as a single-package fallback. Private packages ("private": true) are always excl
|
|
|
563
603
|
Examples:
|
|
564
604
|
trustci --dry-run --provider github --repo me/repo --file release.yml --allow-publish
|
|
565
605
|
trustci --provider gitlab --project me/repo --file .gitlab-ci.yml --allow-publish -y
|
|
566
|
-
trustci --provider circleci --vcs-origin
|
|
606
|
+
trustci --provider circleci --vcs-origin github.com/me/repo \\
|
|
567
607
|
--circle-token $CIRCLECI_TOKEN --allow-publish
|
|
568
608
|
trustci --provider circleci --org-id <uuid> --project-id <uuid> \\
|
|
569
|
-
--pipeline-definition-id <uuid> --vcs-origin
|
|
609
|
+
--pipeline-definition-id <uuid> --vcs-origin github.com/me/repo --allow-publish
|
|
570
610
|
trustci list
|
|
571
611
|
`;
|
|
572
612
|
async function confirm(question) {
|
|
@@ -711,25 +751,11 @@ async function main() {
|
|
|
711
751
|
}
|
|
712
752
|
if (parsed.kind === "list")
|
|
713
753
|
return runList(parsed.pkg, parsed.dryRun);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
console.error("Resolving org-id and project-id via CircleCI API...");
|
|
720
|
-
const resolved = await resolveOrgAndProjectId(opts.circleToken, opts.vcsOrigin);
|
|
721
|
-
opts.orgId ??= resolved.orgId;
|
|
722
|
-
opts.projectId ??= resolved.projectId;
|
|
723
|
-
}
|
|
724
|
-
if (!opts.pipelineDefinitionId) {
|
|
725
|
-
console.error("Resolving pipeline-definition-id via CircleCI API...");
|
|
726
|
-
opts.pipelineDefinitionId = await resolvePipelineDefinitionId(opts.circleToken, opts.projectId, opts.file);
|
|
727
|
-
}
|
|
728
|
-
} catch (err) {
|
|
729
|
-
console.error(`error: ${err.message}`);
|
|
730
|
-
return 1;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
754
|
+
try {
|
|
755
|
+
await resolveCircleciOptions(parsed);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
console.error(`error: ${err.message}`);
|
|
758
|
+
return 1;
|
|
733
759
|
}
|
|
734
760
|
return runTrust(parsed);
|
|
735
761
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trustci",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "bun build src/cli.ts --target node --outfile dist/cli.js",
|
|
18
18
|
"test": "bun test",
|
|
19
|
-
"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "bun run build",
|
|
21
|
+
"prepare": "husky"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
22
24
|
"npm",
|
|
@@ -30,9 +32,24 @@
|
|
|
30
32
|
"gitlab",
|
|
31
33
|
"circleci"
|
|
32
34
|
],
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Hidetaka Okamoto",
|
|
37
|
+
"url": "https://github.com/hideokamoto"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/hideokamoto/trustci.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/hideokamoto/trustci#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/hideokamoto/trustci/issues"
|
|
46
|
+
},
|
|
33
47
|
"license": "MIT",
|
|
34
48
|
"devDependencies": {
|
|
49
|
+
"@commitlint/cli": "^21.2.3",
|
|
50
|
+
"@commitlint/config-conventional": "^21.2.3",
|
|
35
51
|
"@types/bun": "latest",
|
|
52
|
+
"husky": "^9.1.7",
|
|
36
53
|
"typescript": "^5"
|
|
37
54
|
}
|
|
38
55
|
}
|