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.
Files changed (3) hide show
  1. package/README.md +28 -3
  2. package/dist/cli.js +108 -82
  3. 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 <url>` | `--context-id` (repeatable) |
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 must include the protocol)
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 https://github.com/me/repo \
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
- const vcsOrigin = require_(values["vcs-origin"], "vcs-origin", "circleci");
106
- if (!vcsOrigin.includes("://")) {
107
- throw new ArgError(`--vcs-origin must include a protocol, e.g. https://github.com/org/repo (got "${vcsOrigin}").`);
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 = "0.1.0";
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 <url> --circle-token <token> [--file <config>]
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 https://github.com/me/repo \\
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 https://github.com/me/repo --allow-publish
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
- if (parsed.options.provider === "circleci" && !parsed.dryRun) {
715
- const opts = parsed.options;
716
- if (opts.circleToken) {
717
- try {
718
- if (!opts.orgId || !opts.projectId) {
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.0",
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
- "prepublishOnly": "bun run build"
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
  }