@decohere/ci-tools 0.0.1-dev.123 → 0.0.1-dev.125

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/ci-tools",
3
- "version": "0.0.1-dev.123",
3
+ "version": "0.0.1-dev.125",
4
4
  "description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
5
5
  "keywords": [
6
6
  "typescript",
@@ -7,14 +7,13 @@ const MS_PER_DAY = 86_400_000;
7
7
  const DEFAULT_REPO = "typesmiths/decohere";
8
8
 
9
9
  /**
10
- * One profile per credential. Date math, GitHub issue upsert, and fail-closed live below.
11
- * `ghEnv` unset means repo secret / repo variable (not an Actions environment).
10
+ * One profile per credential (repo secret + repo variable). Date math, GitHub issue
11
+ * upsert, and fail-closed live below.
12
12
  *
13
13
  * @typedef {{
14
14
  * id: string,
15
15
  * secret: string,
16
16
  * expiresVar: string,
17
- * ghEnv?: string,
18
17
  * mint: string,
19
18
  * failImpact: string,
20
19
  * rejectedWhy: string,
@@ -24,17 +23,6 @@ const DEFAULT_REPO = "typesmiths/decohere";
24
23
 
25
24
  /** @type {Record<string, CredentialProfile>} */
26
25
  export const CREDENTIALS = {
27
- ghcr: {
28
- id: "ghcr",
29
- secret: "GHCR_PUSH_TOKEN",
30
- expiresVar: "GHCR_TOKEN_EXPIRES_AT",
31
- ghEnv: "ghcr-publish",
32
- mint: "Create a **classic** GitHub PAT (fine-grained tokens cannot push to GHCR): https://github.com/settings/tokens/new?scopes=write:packages",
33
- failImpact:
34
- "npm may already have published. Docker login/push and the `v*` tag will **fail**. The SHA stays untagged, so the next successful Publish of that tip can finish Docker + tag after you rotate. Images are not pushed with `GITHUB_TOKEN`.",
35
- rejectedWhy: "GHCR `docker login` returned 401 (expired, revoked, or wrong secret name).",
36
- revokeUrl: "https://github.com/settings/tokens",
37
- },
38
26
  npm: {
39
27
  id: "npm",
40
28
  secret: "NODE_AUTH_TOKEN",
@@ -85,11 +73,6 @@ export function resolveRepo() {
85
73
  return process.env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
86
74
  }
87
75
 
88
- /** @param {CredentialProfile} credential */
89
- export function ghScopeFlags(credential) {
90
- return credential.ghEnv ? `--env ${credential.ghEnv} ` : "";
91
- }
92
-
93
76
  /** @param {string} id */
94
77
  export function credentialProfile(id) {
95
78
  const credential = CREDENTIALS[id];
@@ -109,15 +92,14 @@ export function issueTitle(credential, expiresYmd) {
109
92
  /** @param {CredentialProfile} credential */
110
93
  export function rotationCommands(credential, nextExpiresYmd, repo = resolveRepo()) {
111
94
  const next = nextExpiresYmd ?? "YYYY-MM-DD";
112
- const flags = ghScopeFlags(credential);
113
95
  return [
114
96
  credential.mint,
115
97
  "",
116
98
  "Then from a trusted machine (do not paste the token into Actions or this issue):",
117
99
  "",
118
100
  "```bash",
119
- `gh secret set ${credential.secret} ${flags}--repo ${repo}`,
120
- `gh variable set ${credential.expiresVar} ${flags}--repo ${repo} --body ${next}`,
101
+ `gh secret set ${credential.secret} --repo ${repo}`,
102
+ `gh variable set ${credential.expiresVar} --repo ${repo} --body ${next}`,
121
103
  "```",
122
104
  "",
123
105
  `Revoke the old token: ${credential.revokeUrl}`,
@@ -199,13 +181,12 @@ export async function runNudge(env = process.env, upsert = upsertIssue) {
199
181
  const expiresYmd = String(env[credential.expiresVar] ?? "").trim();
200
182
  const expiresAtMs = parseExpiresAt(expiresYmd);
201
183
  const assessment = assessTokenLifetime({ expiresAtMs });
202
- const flags = ghScopeFlags(credential);
203
184
  const repo = env.GITHUB_REPOSITORY?.trim() || DEFAULT_REPO;
204
185
 
205
186
  if (assessment.kind === "missing" && !rejected) {
206
187
  process.stderr.write(
207
- `${credential.ghEnv ? `${credential.ghEnv} environment` : "repository"} variable ${credential.expiresVar}=YYYY-MM-DD is required\n` +
208
- ` gh variable set ${credential.expiresVar} ${flags}--repo ${repo} --body YYYY-MM-DD\n`,
188
+ `repository variable ${credential.expiresVar}=YYYY-MM-DD is required\n` +
189
+ ` gh variable set ${credential.expiresVar} --repo ${repo} --body YYYY-MM-DD\n`,
209
190
  );
210
191
  return 1;
211
192
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * First-publish a new package name (restricted) and give it its Trusted Publisher,
2
+ * First-publish a new package name (public) and give it its Trusted Publisher,
3
3
  * so `publish.yml` can release it over OIDC from then on.
4
4
  *
5
5
  * pnpm npm:bootstrap [--otp <code>] [--dry-run] [--channel dev] [<package-name>...]
@@ -31,15 +31,15 @@ export function needsPrereleaseTag(version) {
31
31
  }
32
32
 
33
33
  /**
34
- * npm arguments for a name-only first publish. Restricted is the scoped default,
35
- * so no `--access`. `./` stops npm reading the path as a GitHub `owner/repo`.
34
+ * npm arguments for a name-only first publish. Scoped names default to restricted,
35
+ * so `--access public`. `./` stops npm reading the path as a GitHub `owner/repo`.
36
36
  *
37
37
  * @param {{ dir: string, version?: string }} pkg
38
38
  * @param {string} channel
39
39
  */
40
40
  export function publishArgs(pkg, channel) {
41
41
  const tag = needsPrereleaseTag(pkg.version) ? ["--tag", channel] : [];
42
- return ["publish", `./projects/${pkg.dir}`, ...tag];
42
+ return ["publish", `./projects/${pkg.dir}`, "--access", "public", ...tag];
43
43
  }
44
44
 
45
45
  /** @param {string} name */
@@ -33,7 +33,7 @@ export function firstPublishCommands(packages, channel = "dev") {
33
33
  }
34
34
  return packages.map((pkg) => {
35
35
  const tag = needsPrereleaseTag(pkg.version) ? ` --tag ${channel}` : "";
36
- return `npm publish ./projects/${pkg.dir}${tag}`;
36
+ return `npm publish ./projects/${pkg.dir} --access public${tag}`;
37
37
  });
38
38
  }
39
39
 
@@ -95,7 +95,7 @@ export function buildDescription(options) {
95
95
  "",
96
96
  "That does steps 1 and 2 below, skipping what is already done. To do them by hand instead:",
97
97
  "",
98
- "1. First-publish each name **restricted** (scoped default — do **not** pass `--access public`). From a trusted machine, `npm login` to the org, then from the **repo root**. Use `./` so npm does not treat the path as a GitHub `owner/repo`. `--tag` is required for prerelease versions (`x.y.z-dev.N`). Missing `bin`/`es6` warnings are OK for this name-only bootstrap; CI `forge publish` ships the built tarball later:",
98
+ "1. First-publish each name **public** (`--access public`: scoped names default to restricted). From a trusted machine, `npm login` to the org, then from the **repo root**. Use `./` so npm does not treat the path as a GitHub `owner/repo`. `--tag` is required for prerelease versions (`x.y.z-dev.N`). Missing `bin`/`es6` warnings are OK for this name-only bootstrap; CI `forge publish` ships the built tarball later:",
99
99
  "",
100
100
  "```bash",
101
101
  ...firstPublishCommands(packages, channel),
@@ -4,7 +4,6 @@ import {
4
4
  buildDescription,
5
5
  CREDENTIALS,
6
6
  credentialProfile,
7
- ghScopeFlags,
8
7
  issueTitle,
9
8
  parseExpiresAt,
10
9
  rotationCommands,
@@ -54,14 +53,8 @@ describe("credential-nudge", () => {
54
53
  expect(assessTokenLifetime({ expiresAtMs: null }).kind).toBe("missing");
55
54
  });
56
55
 
57
- it("parameterizes GHCR vs npm rotate commands without duplicating date logic", () => {
58
- const ghcr = credentialProfile("ghcr");
56
+ it("builds the rotation issue for a credential profile", () => {
59
57
  const npm = credentialProfile("npm");
60
- expect(ghScopeFlags(ghcr)).toBe("--env ghcr-publish ");
61
- expect(ghScopeFlags(npm)).toBe("");
62
- expect(rotationCommands(ghcr, "2027-02-17", "typesmiths/decohere")).toContain(
63
- "gh secret set GHCR_PUSH_TOKEN --env ghcr-publish --repo typesmiths/decohere",
64
- );
65
58
  expect(rotationCommands(npm, "2027-02-17", "typesmiths/decohere")).toContain(
66
59
  "gh secret set NODE_AUTH_TOKEN --repo typesmiths/decohere",
67
60
  );
@@ -71,16 +64,15 @@ describe("credential-nudge", () => {
71
64
  expect(issueTitle(npm, "2026-11-19")).toBe(
72
65
  "chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)",
73
66
  );
74
- const body = buildDescription(ghcr, {
67
+ const body = buildDescription(npm, {
75
68
  kind: "warn",
76
69
  expiresYmd: "2026-11-19",
77
70
  daysLeft: 14,
78
71
  rejected: false,
79
72
  });
80
- expect(body).toContain("Docker login/push and the `v*` tag will **fail**");
81
- expect(CREDENTIALS.npm.secret).toBe("NODE_AUTH_TOKEN");
73
+ expect(body).toContain(CREDENTIALS.npm.failImpact);
82
74
  expect(suggestedNextExpiry(Date.UTC(2026, 7, 21), 90)).toBe("2026-11-19");
83
- expect(() => credentialProfile("nope")).toThrow(/Unknown NUDGE_CREDENTIAL/);
75
+ expect(() => credentialProfile("ghcr")).toThrow(/Unknown NUDGE_CREDENTIAL/);
84
76
  });
85
77
 
86
78
  it("files nothing and passes while the token has more than 14 days left", async () => {
@@ -104,13 +96,17 @@ describe("credential-nudge", () => {
104
96
 
105
97
  it("files a rotation issue and stops Publish once the token has expired or was rejected", async () => {
106
98
  const expired = recordUpserts();
107
- const env = { NUDGE_CREDENTIAL: "ghcr", GHCR_TOKEN_EXPIRES_AT: inDays(0) };
99
+ const env = { NUDGE_CREDENTIAL: "npm", NODE_AUTH_TOKEN_EXPIRES_AT: inDays(0) };
108
100
  expect(await runNudge(env, expired.upsert)).toBe(1);
109
101
  expect(expired.upserts).toHaveLength(1);
110
102
 
111
103
  const rejected = recordUpserts();
112
- const rejectedEnv = { ...env, GHCR_TOKEN_EXPIRES_AT: inDays(60), NUDGE_REJECTED: "1" };
104
+ const rejectedEnv = {
105
+ ...env,
106
+ NODE_AUTH_TOKEN_EXPIRES_AT: inDays(60),
107
+ NUDGE_REJECTED: "1",
108
+ };
113
109
  expect(await runNudge(rejectedEnv, rejected.upsert)).toBe(1);
114
- expect(rejected.upserts[0].body).toContain(CREDENTIALS.ghcr.rejectedWhy);
110
+ expect(rejected.upserts[0].body).toContain(CREDENTIALS.npm.rejectedWhy);
115
111
  });
116
112
  });
@@ -58,7 +58,7 @@ afterEach(() => {
58
58
  });
59
59
 
60
60
  describe("npm-bootstrap", () => {
61
- it("first-publishes unpublished names restricted, then sets the Trusted Publisher", async () => {
61
+ it("first-publishes unpublished names public, then sets the Trusted Publisher", async () => {
62
62
  seedWorkspace();
63
63
  const registry = fakeNpm({ published: ["@decohere/scribe"] });
64
64
 
@@ -67,7 +67,7 @@ describe("npm-bootstrap", () => {
67
67
  expect(code).toBe(0);
68
68
  expect(registry.calls).toEqual([
69
69
  "whoami",
70
- "publish ./projects/scribe/scribe-console --tag dev [otp 123456]",
70
+ "publish ./projects/scribe/scribe-console --access public --tag dev [otp 123456]",
71
71
  "trust list @decohere/scribe-console --json [otp 123456]",
72
72
  "trust github @decohere/scribe-console --file publish.yml --repo typesmiths/decohere --env npm-publish --allow-publish --yes [otp 123456]",
73
73
  ]);
@@ -107,7 +107,7 @@ describe("npm-bootstrap", () => {
107
107
  expect(code).toBe(0);
108
108
  expect(registry.calls).toEqual(["whoami"]);
109
109
  expect(lines).toContain(
110
- "[dry run] npm publish ./projects/scribe/scribe-console --tag dev",
110
+ "[dry run] npm publish ./projects/scribe/scribe-console --access public --tag dev",
111
111
  );
112
112
  });
113
113
 
@@ -47,16 +47,15 @@ describe("oidc-bootstrap-nudge", () => {
47
47
  expect(needsPrereleaseTag("0.0.1-dev.18")).toBe(true);
48
48
  expect(needsPrereleaseTag("1.2.3")).toBe(false);
49
49
  expect(firstPublishCommands([pkg], "dev")).toEqual([
50
- "npm publish ./projects/scaffold --tag dev",
50
+ "npm publish ./projects/scaffold --access public --tag dev",
51
51
  ]);
52
52
  expect(firstPublishCommands([{ ...pkg, version: "1.0.0" }], "dev")).toEqual([
53
- "npm publish ./projects/scaffold",
53
+ "npm publish ./projects/scaffold --access public",
54
54
  ]);
55
- expect(body).toContain("npm publish ./projects/scaffold --tag dev");
55
+ expect(body).toContain("npm publish ./projects/scaffold --access public --tag dev");
56
56
  expect(body).toContain("pnpm npm:bootstrap --otp <code> @decohere/scaffold");
57
57
  expect(body).not.toMatch(/# @decohere\/scaffold/);
58
- expect(body).toContain("do **not** pass `--access public`");
59
- expect(firstPublishCommands([pkg], "dev").join("\n")).not.toContain("--access public");
58
+ expect(body).toContain("First-publish each name **public**");
60
59
  expect(body).toContain("Workflow filename: `publish.yml`");
61
60
  expect(body).toContain("Environment: `npm-publish`");
62
61
  expect(body).toContain("gh run rerun --failed");