@williamthorsen/release-kit 10.2.2 → 10.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.
@@ -1,6 +1,6 @@
1
1
  /** @noformat — @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.23.0";
3
+ export const __readyupVersion = "0.24.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
@@ -43,7 +43,7 @@ function detectRepoType() {
43
43
  if (existsSync("package.json")) {
44
44
  const raw = readFileSync("package.json", "utf8");
45
45
  const pkg = parseJsonRecord(raw);
46
- if (pkg !== void 0 && Array.isArray(pkg.workspaces)) {
46
+ if (pkg !== void 0 && Array.isArray(pkg["workspaces"])) {
47
47
  return "monorepo";
48
48
  }
49
49
  }
@@ -52,15 +52,21 @@ function detectRepoType() {
52
52
 
53
53
  // .readyup/kits/default.ts
54
54
  function getMinVersion() {
55
- const picked = { "version": "10.2.2" };
56
- if (typeof picked.version !== "string") {
55
+ const picked = { "version": "10.3.0" };
56
+ if (typeof picked["version"] !== "string") {
57
57
  throw new TypeError("release-kit/package.json: 'version' must be a string");
58
58
  }
59
- return picked.version;
59
+ return picked["version"];
60
60
  }
61
61
  function hasPublishablePackages() {
62
62
  return discoverWorkspaces({ filter: (w) => w.isPackage }).length > 0;
63
63
  }
64
+ var CONFIG_EXPORT_PATTERNS = [
65
+ /export\s+default\b/,
66
+ /export\s+(?:const|let|var)\s+config\b/,
67
+ /export\s*\{[^}]*\b(?:config|default)\b[^}]*\}/,
68
+ /export\s*\*/
69
+ ];
64
70
  var CLIFF_TEMPLATE_HASH = "93b72e0b1393cd6b1fe8e2a0e303cd326fd323435951b0493396b305af32d2ec";
65
71
  var COMMON_PRESET_HASH = "86f9e1db9000793a91168e8c6b5695311a422ee208121324549c068fe67fa184";
66
72
  var SYNC_LABELS_WORKFLOW_HASH = "d6e2403fb551d2d415f679125989c92760444eec887644565b2e05c9bf8f4c1e";
@@ -111,6 +117,7 @@ var default_default = defineRdyKit({
111
117
  {
112
118
  name: "release.yaml does not reference deprecated tag ref",
113
119
  severity: "error",
120
+ quiet: true,
114
121
  check: () => fileDoesNotContain(".github/workflows/release.yaml", /@(release|publish)-workflow-v[0-9]/),
115
122
  fix: "Update release.yaml to use @workflow/release-v1 (run `release-kit init --force` to regenerate, or replace the ref manually)"
116
123
  }
@@ -135,50 +142,79 @@ var default_default = defineRdyKit({
135
142
  {
136
143
  name: "publish.yaml does not reference deprecated tag ref",
137
144
  severity: "error",
145
+ quiet: true,
138
146
  check: () => fileDoesNotContain(".github/workflows/publish.yaml", /@(release|publish)-workflow-v[0-9]/),
139
147
  fix: "Update publish.yaml to use @workflow/publish-v1 (run `release-kit init --force` to regenerate, or replace the ref manually)"
140
148
  }
141
149
  ]
142
150
  },
143
151
  {
144
- name: "releaseNotes config is consistent with changelogJson",
145
- severity: "warn",
146
- skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
147
- check: () => releaseNotesConfigIsConsistent(),
148
- fix: "Either enable changelogJson.enabled or disable releaseNotes.shouldInjectIntoReadme"
149
- },
150
- {
151
- name: "config does not use removed releaseNotes.shouldCreateGithubRelease",
152
+ name: ".config/release-kit.config.ts exports a config",
152
153
  severity: "error",
153
154
  skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
154
- check: () => fileDoesNotContain(".config/release-kit.config.ts", /shouldCreateGithubRelease/),
155
- fix: "Remove 'shouldCreateGithubRelease' from .config/release-kit.config.ts. Adoption of GitHub Releases is now signaled by installing the create-github-release workflow (see release-kit README for setup)."
156
- },
157
- {
158
- name: ".config/release-kit.config.ts uses defineConfig",
159
- severity: "recommend",
160
- skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
161
- check: () => fileContains(".config/release-kit.config.ts", /defineConfig/),
162
- fix: "Wrap your config export with defineConfig() from @williamthorsen/release-kit/config for type safety"
163
- },
164
- {
165
- name: "releaseNotes.shouldInjectIntoReadme is true",
166
- severity: "warn",
167
- skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
168
- check: () => releaseNotesInjectsIntoReadme(),
169
- fix: "Set releaseNotes.shouldInjectIntoReadme to true in .config/release-kit.config.ts",
155
+ check: () => configFileExportsConfig(),
156
+ fix: "Export the config from .config/release-kit.config.ts as a default export or as a named `config` export; release-kit resolves no other export",
170
157
  checks: [
171
158
  {
172
- name: "README contains release-notes section markers",
159
+ name: "releaseNotes config is consistent with changelogJson",
160
+ severity: "warn",
161
+ check: () => releaseNotesConfigIsConsistent(),
162
+ fix: "Either enable changelogJson.enabled or disable releaseNotes.shouldInjectIntoReadme"
163
+ },
164
+ {
165
+ name: ".config/release-kit.config.ts uses defineConfig",
166
+ severity: "recommend",
167
+ check: () => fileContains(".config/release-kit.config.ts", /defineConfig/),
168
+ fix: "Wrap your config export with defineConfig() from @williamthorsen/release-kit/config for type safety"
169
+ },
170
+ {
171
+ name: "releaseNotes.shouldInjectIntoReadme is true",
172
+ severity: "warn",
173
+ check: () => releaseNotesInjectsIntoReadme(),
174
+ fix: "Set releaseNotes.shouldInjectIntoReadme to true in .config/release-kit.config.ts",
175
+ checks: [
176
+ {
177
+ name: "README contains release-notes section markers",
178
+ severity: "warn",
179
+ check: readmesHaveReleaseNotesMarkers,
180
+ fix: "Add `<!-- section:release-notes -->` and `<!-- /section:release-notes -->` markers to each affected README"
181
+ }
182
+ ]
183
+ },
184
+ {
185
+ name: "repoLabels block declared in .config/release-kit.config.ts",
186
+ severity: "recommend",
187
+ check: () => fileContains(".config/release-kit.config.ts", /repoLabels/),
188
+ fix: "Run `release-kit sync-labels init` to seed a repoLabels block, then customize labels"
189
+ },
190
+ {
191
+ name: ".github/labels.yaml exists",
173
192
  severity: "warn",
174
- check: readmesHaveReleaseNotesMarkers,
175
- fix: "Add `<!-- section:release-notes -->` and `<!-- /section:release-notes -->` markers to each affected README"
193
+ skip: () => !fileContains(".config/release-kit.config.ts", /repoLabels/) ? "no repoLabels config" : false,
194
+ check: () => fileExists(".github/labels.yaml"),
195
+ fix: "Run `release-kit sync-labels generate` to produce the labels file",
196
+ checks: [
197
+ {
198
+ name: "labels.yaml has current common preset",
199
+ severity: "warn",
200
+ check: () => labelsHaveCurrentPresetHash("common", COMMON_PRESET_HASH),
201
+ fix: "Run `release-kit sync-labels generate` to incorporate updated common labels"
202
+ }
203
+ ]
176
204
  }
177
205
  ]
178
206
  },
207
+ {
208
+ name: "config does not use removed releaseNotes.shouldCreateGithubRelease",
209
+ severity: "error",
210
+ quiet: true,
211
+ check: () => fileDoesNotContain(".config/release-kit.config.ts", /shouldCreateGithubRelease/),
212
+ fix: "Remove 'shouldCreateGithubRelease' from .config/release-kit.config.ts. Adoption of GitHub Releases is now signaled by installing the create-github-release workflow (see release-kit README for setup)."
213
+ },
179
214
  {
180
215
  name: "git-cliff not in devDependencies",
181
216
  severity: "recommend",
217
+ quiet: true,
182
218
  check: () => !hasDevDependency("git-cliff"),
183
219
  fix: "pnpm remove git-cliff \u2014 release-kit handles changelog generation directly"
184
220
  },
@@ -206,41 +242,26 @@ var default_default = defineRdyKit({
206
242
  {
207
243
  name: "sync-labels.yaml does not reference deprecated tag ref",
208
244
  severity: "error",
245
+ quiet: true,
209
246
  check: () => fileDoesNotContain(".github/workflows/sync-labels.yaml", /@sync-labels-workflow-v[0-9]/),
210
247
  fix: "Update sync-labels.yaml to use @workflow/sync-labels-v1 (run `release-kit sync-labels init --force` to regenerate, or replace the ref manually)"
211
248
  },
212
249
  {
213
250
  name: "retired .config/sync-labels.config.ts is absent",
214
251
  severity: "error",
252
+ quiet: true,
215
253
  check: () => !fileExists(".config/sync-labels.config.ts"),
216
254
  fix: "Move the labels into the repoLabels block of .config/release-kit.config.ts, then delete .config/sync-labels.config.ts"
217
- },
218
- {
219
- name: "repoLabels block declared in .config/release-kit.config.ts",
220
- severity: "recommend",
221
- skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
222
- check: () => fileContains(".config/release-kit.config.ts", /repoLabels/),
223
- fix: "Run `release-kit sync-labels init` to seed a repoLabels block, then customize labels"
224
- },
225
- {
226
- name: ".github/labels.yaml exists",
227
- severity: "warn",
228
- skip: () => !fileContains(".config/release-kit.config.ts", /repoLabels/) ? "no repoLabels config" : false,
229
- check: () => fileExists(".github/labels.yaml"),
230
- fix: "Run `release-kit sync-labels generate` to produce the labels file",
231
- checks: [
232
- {
233
- name: "labels.yaml has current common preset",
234
- severity: "warn",
235
- check: () => labelsHaveCurrentPresetHash("common", COMMON_PRESET_HASH),
236
- fix: "Run `release-kit sync-labels generate` to incorporate updated common labels"
237
- }
238
- ]
239
255
  }
240
256
  ]
241
257
  }
242
258
  ]
243
259
  });
260
+ function configFileExportsConfig() {
261
+ const content = readFile(".config/release-kit.config.ts");
262
+ if (content === void 0) return false;
263
+ return CONFIG_EXPORT_PATTERNS.some((pattern) => pattern.test(content));
264
+ }
244
265
  function labelsHaveCurrentPresetHash(presetName, expectedHash) {
245
266
  const content = readFile(".github/labels.yaml");
246
267
  if (content === void 0) return false;
@@ -253,7 +274,8 @@ function readmeHasReleaseNotesMarkers(content) {
253
274
  }
254
275
  function readmesHaveReleaseNotesMarkers() {
255
276
  const failing = [];
256
- for (const { dir } of discoverWorkspaces({ filter: (w) => w.isPackage })) {
277
+ const packageWorkspaces = discoverWorkspaces({ filter: (w) => w.isPackage });
278
+ for (const { dir } of packageWorkspaces) {
257
279
  const readmePath = dir === "." ? "README.md" : `${dir}/README.md`;
258
280
  const content = readFile(readmePath);
259
281
  if (content === void 0 || !readmeHasReleaseNotesMarkers(content)) {
@@ -287,6 +309,7 @@ export {
287
309
  RELEASE_WORKFLOW_HASH_MONOREPO,
288
310
  RELEASE_WORKFLOW_HASH_SINGLE,
289
311
  SYNC_LABELS_WORKFLOW_HASH,
312
+ configFileExportsConfig,
290
313
  default_default as default,
291
314
  readmeHasReleaseNotesMarkers,
292
315
  readmesHaveReleaseNotesMarkers
@@ -1,17 +1,13 @@
1
1
  /** @noformat — @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.23.0";
3
+ export const __readyupVersion = "0.24.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/npm-auto-publish.ts
7
7
  import { execSync } from "node:child_process";
8
8
  import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import path from "node:path";
10
- import {
11
- defineRdyChecklist,
12
- defineRdyKit,
13
- defineRdyStagedChecklist
14
- } from "readyup";
10
+ import { defineRdyChecklist, defineRdyKit } from "readyup";
15
11
  import {
16
12
  discoverWorkspaces,
17
13
  fileContains,
@@ -32,32 +28,32 @@ var UNREACHABLE_ERROR_CODES = /* @__PURE__ */ new Set([
32
28
  "ERR_SOCKET_TIMEOUT",
33
29
  "ETIMEDOUT"
34
30
  ]);
35
- var repoChecklist = defineRdyStagedChecklist({
31
+ var repoChecklist = defineRdyChecklist({
36
32
  name: "repo",
37
- preconditions: [
33
+ checks: [
38
34
  {
39
35
  name: "publish.yaml exists",
36
+ skip: () => skipIfNothingPublishable(),
40
37
  check: () => fileExists(".github/workflows/publish.yaml"),
41
- fix: 'Run "release-kit init" to scaffold the publish workflow, or create .github/workflows/publish.yaml manually'
38
+ fix: 'Run "release-kit init" to scaffold the publish workflow, or create .github/workflows/publish.yaml manually',
39
+ checks: [
40
+ {
41
+ name: "id-token: write permission declared",
42
+ check: () => fileContains(".github/workflows/publish.yaml", /id-token:\s*write/),
43
+ fix: 'Add "permissions: { id-token: write, contents: read }" to .github/workflows/publish.yaml \u2014 required for OIDC-based npm authentication'
44
+ },
45
+ {
46
+ name: "No legacy token references in workflow files",
47
+ quiet: true,
48
+ check: () => !hasTokenReferences(),
49
+ fix: "Remove NPM_TOKEN/NODE_AUTH_TOKEN references from workflow files; OIDC auth replaces token-based auth"
50
+ },
51
+ {
52
+ name: "Provenance setting matches repo visibility",
53
+ check: checkProvenanceMatchesVisibility
54
+ }
55
+ ]
42
56
  }
43
- ],
44
- groups: [
45
- [
46
- {
47
- name: "id-token: write permission declared",
48
- check: () => fileContains(".github/workflows/publish.yaml", /id-token:\s*write/),
49
- fix: 'Add "permissions: { id-token: write, contents: read }" to .github/workflows/publish.yaml \u2014 required for OIDC-based npm authentication'
50
- },
51
- {
52
- name: "No legacy token references in workflow files",
53
- check: () => !hasTokenReferences(),
54
- fix: "Remove NPM_TOKEN/NODE_AUTH_TOKEN references from workflow files; OIDC auth replaces token-based auth"
55
- },
56
- {
57
- name: "Provenance setting matches repo visibility",
58
- check: checkProvenanceMatchesVisibility
59
- }
60
- ]
61
57
  ]
62
58
  });
63
59
  var packagesChecklist = defineRdyChecklist({
@@ -67,7 +63,7 @@ var packagesChecklist = defineRdyChecklist({
67
63
  name: 'packageManager field starts with "pnpm"',
68
64
  check: () => {
69
65
  const rootPkg = readJsonFile("package.json");
70
- const pm = typeof rootPkg?.packageManager === "string" ? rootPkg.packageManager : "";
66
+ const pm = typeof rootPkg?.["packageManager"] === "string" ? rootPkg["packageManager"] : "";
71
67
  return pm.startsWith("pnpm");
72
68
  },
73
69
  fix: 'Set "packageManager": "pnpm@..." in root package.json'
@@ -76,21 +72,24 @@ var packagesChecklist = defineRdyChecklist({
76
72
  name: "At least one workspace discovered",
77
73
  check: () => discoverWorkspaces().length > 0,
78
74
  fix: "Ensure pnpm-workspace.yaml lists package globs, or that a root package.json exists"
79
- },
75
+ }
76
+ ],
77
+ checks: [
80
78
  {
81
79
  name: "npm session is usable",
80
+ skip: () => skipIfNothingPublishable(),
82
81
  check: () => {
83
82
  const auth = getCachedNpmAuthStatus();
84
83
  return auth.status === "authenticated" ? { ok: true } : { ok: false, detail: auth.detail };
85
84
  },
86
- get fix() {
87
- return getCachedNpmAuthStatus().status === "unreachable" ? "Restore access to the npm registry, then re-run; the trusted-publisher check queries it directly" : "Log in to npm: npm login";
85
+ // readyup reads `fix` before it consults `skip`, and again when it validates the kit, so a getter here would
86
+ // reach the registry on every run regardless of the skip. Per-outcome wording goes in the check's detail.
87
+ fix: 'Restore a usable npm session: log in with "npm login", or restore access to the registry, which the trusted-publisher check queries directly',
88
+ get checks() {
89
+ return discoverWorkspaces().map((workspace) => buildWorkspaceCheck(workspace));
88
90
  }
89
91
  }
90
- ],
91
- get checks() {
92
- return discoverWorkspaces().map((workspace) => buildWorkspaceCheck(workspace));
93
- }
92
+ ]
94
93
  });
95
94
  var npm_auto_publish_default = defineRdyKit({
96
95
  fixLocation: "inline",
@@ -102,7 +101,7 @@ function buildWorkspaceCheck(workspace) {
102
101
  const children = [
103
102
  {
104
103
  name: "repository field exists",
105
- check: () => workspace.packageJson.repository !== void 0 && workspace.packageJson.repository !== null,
104
+ check: () => workspace.packageJson["repository"] !== void 0 && workspace.packageJson["repository"] !== null,
106
105
  fix: `Add a "repository" field to ${pkgJsonPath} pointing to the GitHub repo`
107
106
  }
108
107
  ];
@@ -134,7 +133,7 @@ function buildWorkspaceCheck(workspace) {
134
133
  {
135
134
  name: "files field exists",
136
135
  severity: "warn",
137
- check: () => workspace.packageJson.files !== void 0,
136
+ check: () => workspace.packageJson["files"] !== void 0,
138
137
  fix: `Add a "files" field to ${pkgJsonPath} to control which files are included in the published tarball`
139
138
  }
140
139
  );
@@ -237,18 +236,14 @@ function describeTrustRelationships(relationships) {
237
236
  return `${type} ${repository} (${file})`;
238
237
  }).join(", ");
239
238
  }
240
- var cachedNpmAuthStatus;
241
- function getCachedNpmAuthStatus() {
242
- cachedNpmAuthStatus ??= classifyNpmAuth(runNpmJson("npm whoami --json"));
243
- return cachedNpmAuthStatus;
244
- }
245
- var cachedOwnerRepo;
246
- function getCachedOwnerRepo() {
247
- if (cachedOwnerRepo === void 0) {
248
- cachedOwnerRepo = getOwnerRepo();
249
- }
250
- return cachedOwnerRepo;
251
- }
239
+ var getCachedNpmAuthStatus = /* @__PURE__ */ (() => {
240
+ let cached;
241
+ return () => cached ??= classifyNpmAuth(runNpmJson("npm whoami --json"));
242
+ })();
243
+ var getCachedOwnerRepo = /* @__PURE__ */ (() => {
244
+ let cached;
245
+ return () => cached ??= getOwnerRepo();
246
+ })();
252
247
  function getOwnerRepo() {
253
248
  const url = execSync("git remote get-url origin", {
254
249
  encoding: "utf8"
@@ -305,10 +300,10 @@ function readNpmError(stdout) {
305
300
  } catch {
306
301
  return void 0;
307
302
  }
308
- if (!isRecord(parsed) || !isRecord(parsed.error) || typeof parsed.error.code !== "string") {
303
+ if (!isRecord(parsed) || !isRecord(parsed["error"]) || typeof parsed["error"]["code"] !== "string") {
309
304
  return void 0;
310
305
  }
311
- const { code, summary } = parsed.error;
306
+ const { code, summary } = parsed["error"];
312
307
  return { code, summary: typeof summary === "string" ? summary : "" };
313
308
  }
314
309
  function readTrustRelationships(stdout) {
@@ -330,10 +325,14 @@ function runNpmJson(command) {
330
325
  try {
331
326
  return { exitOk: true, stdout: execSync(command, { encoding: "utf8", stdio: "pipe" }) };
332
327
  } catch (error) {
333
- const stdout = isRecord(error) && typeof error.stdout === "string" ? error.stdout : "";
328
+ const stdout = isRecord(error) && typeof error["stdout"] === "string" ? error["stdout"] : "";
334
329
  return { exitOk: false, stdout };
335
330
  }
336
331
  }
332
+ function skipIfNothingPublishable() {
333
+ const publishable = discoverWorkspaces({ filter: (workspace) => workspace.isPackage });
334
+ return publishable.length > 0 ? false : "no publishable packages";
335
+ }
337
336
  function skipIfNotPublishable(workspace) {
338
337
  return workspace.isPackage ? false : "package.json#private is true";
339
338
  }
@@ -343,5 +342,6 @@ export {
343
342
  classifyTrustQuery,
344
343
  npm_auto_publish_default as default,
345
344
  packagesChecklist,
346
- skipIfNotPublishable
345
+ skipIfNotPublishable,
346
+ skipIfNothingPublishable
347
347
  };
@@ -7,10 +7,10 @@
7
7
  ],
8
8
  "name": "default",
9
9
  "path": "kits/default.js",
10
- "readyupVersion": "0.23.0",
10
+ "readyupVersion": "0.24.0",
11
11
  "source": "kits/default.ts",
12
- "sourceHash": "afe26920",
13
- "targetHash": "fecaa9b1"
12
+ "sourceHash": "fe239d01",
13
+ "targetHash": "9bd8fb5d"
14
14
  },
15
15
  {
16
16
  "checklists": [
@@ -19,10 +19,10 @@
19
19
  ],
20
20
  "name": "npm-auto-publish",
21
21
  "path": "kits/npm-auto-publish.js",
22
- "readyupVersion": "0.23.0",
22
+ "readyupVersion": "0.24.0",
23
23
  "source": "kits/npm-auto-publish.ts",
24
- "sourceHash": "5b58afac",
25
- "targetHash": "610ed8a7"
24
+ "sourceHash": "ac484613",
25
+ "targetHash": "f790e7d5"
26
26
  }
27
27
  ]
28
28
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 10.3.0 — 2026-08-08
6
+
7
+ ### 🎉 Features
8
+
9
+ - Stop reporting absence checks that pass (#610)
10
+
11
+ A ReadyUp check that checks for a problem (such as an unwanted file or a stale config reference) now stays silent unless it finds one. The `nmr`, `release-kit`, and `v11y-check` ReadyUp kits previously printed a line for every such check on every run.
12
+
13
+ - Stop reporting non-issues in the readyup kits (#613)
14
+
15
+ Modifies the `release-kit` and `nmr` ReadyUp kits to avoid verbose reporting when an initial check determines that further checks aren't applicable. In particular, a repo with no `release-kit` config file gets a single line in the `release-kit` report, and a repo that publishes nothing now bypasses the `npm-auto-publish` kit instead of failing it.
16
+
17
+ ### 🐛 Bug fixes
18
+
19
+ - Publish build output atomically so a failed build cannot destroy it (#631)
20
+
21
+ Fixes an issue where a failed or still-running `nmr build` could leave a package with no build output at all, breaking the tooling that a subsequent build depends on. Separately, the error reported when a package's build output is missing now names the correct recovery command.
22
+
23
+ ### ♻️ Refactoring
24
+
25
+ - Retire the deferred-lint-rule list and enforce its rules (#628)
26
+
27
+ Errors that `release-kit` rethrows now carry the original error as their cause, previously available only as message text. Help text and documentation for `--with-release-notes` now use the same placeholder notation for its output paths.
28
+
29
+ All violations of lint rules in the `release-kit` project are now cleared, and the list of deferred lint rules is retired, restoring associated rules to a max severity of "error" during strict linting.
30
+
31
+ ### ⚙️ Tooling
32
+
33
+ - Migrate to the shared tsconfig baseline (#626)
34
+
35
+ Adopts `@williamthorsen/tsconfig` as the standard TypeScript configuration for this repo, replacing the previous hand-maintained copy. Reading a property through an index signature now requires bracket notation or a type that declares the property. The `nmr` build now fails on a base TypeScript config it cannot resolve, rather than building without it, and it now resolves a base config named by package name alone.
36
+
5
37
  ## 10.2.2 — 2026-08-05
6
38
 
7
39
  ### 🐛 Bug fixes
package/README.md CHANGED
@@ -5,21 +5,23 @@ Version-bumping and changelog-generation toolkit for release workflows.
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v10.2.2 (2026-08-05)
8
+ ## Release notes — v10.3.0 (2026-08-08)
9
9
 
10
- ### 🐛 Bug fixes
10
+ ### 🎉 Features
11
+
12
+ - Stop reporting absence checks that pass (#610)
13
+
14
+ A ReadyUp check that checks for a problem (such as an unwanted file or a stale config reference) now stays silent unless it finds one. The `nmr`, `release-kit`, and `v11y-check` ReadyUp kits previously printed a line for every such check on every run.
11
15
 
12
- - Report the missing npm login once, not per package (#598)
16
+ - Stop reporting non-issues in the readyup kits (#613)
13
17
 
14
- Fixes an issue where running release-kit's publish-readiness checks without an npm login reported a trusted-publisher failure for every published package. The missing login is now reported once, with `npm login` as the fix. An unreachable registry now has its own check. The trusted publisher check produces more informative failure messages.
18
+ Modifies the `release-kit` and `nmr` ReadyUp kits to avoid verbose reporting when an initial check determines that further checks aren't applicable. In particular, a repo with no `release-kit` config file gets a single line in the `release-kit` report, and a repo that publishes nothing now bypasses the `npm-auto-publish` kit instead of failing it.
19
+
20
+ ### 🐛 Bug fixes
15
21
 
16
- - Make prepare all-or-nothing and report the tags file by path (#601)
22
+ - Publish build output atomically so a failed build cannot destroy it (#631)
17
23
 
18
- Fixes an issue where a failed `prepare` could leave the release flow in a broken intermediate state. A release now lands completely or not at all, and a formatting failure no longer discards one that already landed. When a write fails partway, the error names every file that reached disk, every file that did not, and the commands that undo them.
19
-
20
- `commit` and `tag` now report the full path of the tags file they looked for, and distinguish a missing file from one that could not be read, so that running from the wrong directory or worktree is no longer reported as a release that never happened.
21
-
22
- Runs with `--with-release-notes` now list the preview files they write, and `--dry-run` names the ones a real run would write.
24
+ Fixes an issue where a failed or still-running `nmr build` could leave a package with no build output at all, breaking the tooling that a subsequent build depends on. Separately, the error reported when a package's build output is missing now names the correct recovery command.
23
25
  <!-- /section:release-notes -->
24
26
 
25
27
  ## Installation
@@ -573,7 +575,7 @@ Run release preparation with automatic workspace discovery.
573
575
  | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode (rejected when a `project` block is configured). |
574
576
  | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag. In monorepo/project mode, defaults to patch (combine with `--bump=X` for a different level); in single-package mode, a bare `--force` is rejected — pass `--bump=major\|minor\|patch`. |
575
577
  | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
576
- | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
578
+ | `--with-release-notes` | Write per-workspace release-notes previews under `<workspacePath>/docs/` |
577
579
  | `--help`, `-h` | Show help |
578
580
 
579
581
  Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
@@ -582,8 +584,8 @@ Workspace names for `--only` match the package directory name (e.g., `arrays`, `
582
584
 
583
585
  `--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
584
586
 
585
- - `{workspacePath}/docs/README.v{version}.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
586
- - `{workspacePath}/docs/RELEASE_NOTES.v{version}.md` — the standalone release notes for this version.
587
+ - `<workspacePath>/docs/README.v<version>.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
588
+ - `<workspacePath>/docs/RELEASE_NOTES.v<version>.md` — the standalone release notes for this version.
587
589
 
588
590
  The publish-time inject-and-revert lifecycle is unchanged; previews are additive, deterministic, and safe to regenerate. When `changelogJson.enabled` is `false`, the flag logs a warning and skips preview generation. In dry-run mode, planned writes are logged and no files are created.
589
591
 
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/bin/release-kit.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('release-kit: build output not found — run `pnpm run build` first\n');
6
+ process.stderr.write('release-kit: build output not found — run `nmr build` first\n');
7
7
  } else {
8
8
  process.stderr.write(`release-kit: failed to load: ${error.message}\n`);
9
9
  }
@@ -263,7 +263,7 @@ if (command === '--version' || command === '-V') {
263
263
  console.info(VERSION);
264
264
  process.exit(0);
265
265
  }
266
- if (command === '--help' || command === '-h' || command === undefined) {
266
+ if (command === undefined || ['--help', '-h'].includes(command)) {
267
267
  showUsage();
268
268
  process.exit(0);
269
269
  }
@@ -350,7 +350,7 @@ if (command === 'init') {
350
350
  if (command === 'sync-labels') {
351
351
  const subcommand = flags[0];
352
352
  const subflags = flags.slice(1);
353
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
353
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
354
354
  showSyncLabelsHelp();
355
355
  process.exit(0);
356
356
  }
@@ -398,7 +398,7 @@ if (command === 'sync-labels') {
398
398
  if (command === 'overrides') {
399
399
  const subcommand = flags[0];
400
400
  const subflags = flags.slice(1);
401
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
401
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
402
402
  showOverridesHelp();
403
403
  process.exit(0);
404
404
  }
@@ -427,7 +427,7 @@ if (command === 'overrides') {
427
427
  if (command === 'work-types') {
428
428
  const subcommand = flags[0];
429
429
  const subflags = flags.slice(1);
430
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
430
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
431
431
  showWorkTypesHelp();
432
432
  process.exit(0);
433
433
  }
@@ -20,7 +20,8 @@ export function buildChangelogEntries(config, tag, options) {
20
20
  if (options?.tagPattern !== undefined) {
21
21
  cliffArgs.push('--tag-pattern', options.tagPattern);
22
22
  }
23
- for (const includePath of options?.includePaths ?? []) {
23
+ const includePaths = options?.includePaths ?? [];
24
+ for (const includePath of includePaths) {
24
25
  cliffArgs.push('--include-path', includePath);
25
26
  }
26
27
  try {
@@ -30,7 +31,7 @@ export function buildChangelogEntries(config, tag, options) {
30
31
  return transformReleases(releases, devOnlySections);
31
32
  }
32
33
  catch (error) {
33
- throw new Error(`Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`);
34
+ throw new Error(`Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
34
35
  }
35
36
  }
36
37
  function parseCliffContext(json) {
@@ -45,14 +46,14 @@ function toCliffContextRelease(value) {
45
46
  return {};
46
47
  }
47
48
  const release = {};
48
- if (typeof value.version === 'string') {
49
- release.version = value.version;
49
+ if (typeof value['version'] === 'string') {
50
+ release.version = value['version'];
50
51
  }
51
- if (typeof value.timestamp === 'number') {
52
- release.timestamp = value.timestamp;
52
+ if (typeof value['timestamp'] === 'number') {
53
+ release.timestamp = value['timestamp'];
53
54
  }
54
- if (isUnknownArray(value.commits)) {
55
- release.commits = value.commits.map(toCliffContextCommit);
55
+ if (isUnknownArray(value['commits'])) {
56
+ release.commits = value['commits'].map(toCliffContextCommit);
56
57
  }
57
58
  return release;
58
59
  }
@@ -61,13 +62,13 @@ function toCliffContextCommit(value) {
61
62
  return { message: '' };
62
63
  }
63
64
  const commit = {
64
- message: typeof value.message === 'string' ? value.message : '',
65
+ message: typeof value['message'] === 'string' ? value['message'] : '',
65
66
  };
66
- if (typeof value.group === 'string') {
67
- commit.group = value.group;
67
+ if (typeof value['group'] === 'string') {
68
+ commit.group = value['group'];
68
69
  }
69
- if (typeof value.id === 'string') {
70
- commit.id = value.id;
70
+ if (typeof value['id'] === 'string') {
71
+ commit.id = value['id'];
71
72
  }
72
73
  return commit;
73
74
  }
@@ -81,7 +82,8 @@ function transformReleases(releases, devOnlySections) {
81
82
  const version = extractVersion(release.version);
82
83
  const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
83
84
  const sectionMap = new Map();
84
- for (const commit of release.commits ?? []) {
85
+ const commits = release.commits ?? [];
86
+ for (const commit of commits) {
85
87
  const group = stripCommentPrefix(commit.group ?? 'Other');
86
88
  const description = extractDescription(commit.message);
87
89
  const body = extractBody(commit.message);
@@ -51,14 +51,18 @@ function readPackageJsonSubset(filePath) {
51
51
  content = readFileSync(filePath, 'utf8');
52
52
  }
53
53
  catch (error) {
54
- throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
54
+ throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
55
+ cause: error,
56
+ });
55
57
  }
56
58
  let parsed;
57
59
  try {
58
60
  parsed = JSON.parse(content);
59
61
  }
60
62
  catch (error) {
61
- throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
63
+ throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
64
+ cause: error,
65
+ });
62
66
  }
63
67
  if (!isPackageJsonSubset(parsed)) {
64
68
  throw new Error(`Invalid package.json at ${filePath}`);
@@ -3,21 +3,21 @@ import { isRecord, isUnknownArray } from "./typeGuards.js";
3
3
  export function isChangelogItem(value) {
4
4
  if (!isRecord(value))
5
5
  return false;
6
- if (typeof value.description !== 'string')
6
+ if (typeof value['description'] !== 'string')
7
7
  return false;
8
- if (value.body !== undefined && typeof value.body !== 'string')
8
+ if (value['body'] !== undefined && typeof value['body'] !== 'string')
9
9
  return false;
10
- if (value.breaking !== undefined && typeof value.breaking !== 'boolean')
10
+ if (value['breaking'] !== undefined && typeof value['breaking'] !== 'boolean')
11
11
  return false;
12
- if (value.hash !== undefined && typeof value.hash !== 'string')
12
+ if (value['hash'] !== undefined && typeof value['hash'] !== 'string')
13
13
  return false;
14
14
  return true;
15
15
  }
16
16
  export function isChangelogEntry(value) {
17
17
  return (isRecord(value) &&
18
- typeof value.version === 'string' &&
19
- typeof value.date === 'string' &&
20
- isUnknownArray(value.sections));
18
+ typeof value['version'] === 'string' &&
19
+ typeof value['date'] === 'string' &&
20
+ isUnknownArray(value['sections']));
21
21
  }
22
22
  export function extractVersion(tag) {
23
23
  const match = /(\d+\.\d+\.\d+.*)$/.exec(tag);
@@ -66,8 +66,8 @@ function validateSingleOverride(key, rawEntry, errors) {
66
66
  entryValid = false;
67
67
  }
68
68
  const result = {};
69
- if (rawEntry.audience !== undefined) {
70
- const audienceResult = validateAudience(key, rawEntry.audience, errors);
69
+ if (rawEntry['audience'] !== undefined) {
70
+ const audienceResult = validateAudience(key, rawEntry['audience'], errors);
71
71
  if (audienceResult === undefined) {
72
72
  entryValid = false;
73
73
  }
@@ -75,31 +75,31 @@ function validateSingleOverride(key, rawEntry, errors) {
75
75
  result.audience = audienceResult;
76
76
  }
77
77
  }
78
- if (rawEntry.description !== undefined) {
79
- if (typeof rawEntry.description !== 'string') {
78
+ if (rawEntry['description'] !== undefined) {
79
+ if (typeof rawEntry['description'] !== 'string') {
80
80
  errors.push(`overrides['${key}']: 'description' must be a string`);
81
81
  entryValid = false;
82
82
  }
83
83
  else {
84
- result.description = rawEntry.description;
84
+ result.description = rawEntry['description'];
85
85
  }
86
86
  }
87
- if (rawEntry.body !== undefined) {
88
- if (typeof rawEntry.body !== 'string') {
87
+ if (rawEntry['body'] !== undefined) {
88
+ if (typeof rawEntry['body'] !== 'string') {
89
89
  errors.push(`overrides['${key}']: 'body' must be a string`);
90
90
  entryValid = false;
91
91
  }
92
92
  else {
93
- result.body = rawEntry.body;
93
+ result.body = rawEntry['body'];
94
94
  }
95
95
  }
96
- if (rawEntry.breaking !== undefined) {
97
- if (typeof rawEntry.breaking !== 'boolean') {
96
+ if (rawEntry['breaking'] !== undefined) {
97
+ if (typeof rawEntry['breaking'] !== 'boolean') {
98
98
  errors.push(`overrides['${key}']: 'breaking' must be a boolean`);
99
99
  entryValid = false;
100
100
  }
101
101
  else {
102
- result.breaking = rawEntry.breaking;
102
+ result.breaking = rawEntry['breaking'];
103
103
  }
104
104
  }
105
105
  if (Object.keys(result).length === 0 && entryValid) {
@@ -236,7 +236,8 @@ export function loadOverridesForScopes(scopes) {
236
236
  project = result.overrides;
237
237
  }
238
238
  }
239
- for (const workspacePath of scopes.workspaces ?? []) {
239
+ const workspacePaths = scopes.workspaces ?? [];
240
+ for (const workspacePath of workspacePaths) {
240
241
  const result = loadChangelogOverrides(resolveOverridePath(workspacePath));
241
242
  if ('errors' in result) {
242
243
  errors.push(...result.errors);
@@ -80,7 +80,7 @@ function stripLocalOnlyFields(value) {
80
80
  return value;
81
81
  }
82
82
  const record = { ...value };
83
- delete record.$schema;
83
+ delete record['$schema'];
84
84
  return record;
85
85
  }
86
86
  function deepEqual(a, b) {
@@ -51,6 +51,6 @@ function assertCleanWorkingTree() {
51
51
  if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
52
52
  throw error;
53
53
  }
54
- throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.');
54
+ throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.', { cause: error });
55
55
  }
56
56
  }
@@ -10,14 +10,14 @@ export function deriveWorkspaceConfig(workspacePath) {
10
10
  }
11
11
  catch (error) {
12
12
  const message = error instanceof Error ? error.message : String(error);
13
- throw new Error(`Failed to read ${packageJsonPath}: ${message}`);
13
+ throw new Error(`Failed to read ${packageJsonPath}: ${message}`, { cause: error });
14
14
  }
15
- const name = isRecord(parsed) ? parsed.name : undefined;
15
+ const name = isRecord(parsed) ? parsed['name'] : undefined;
16
16
  if (typeof name !== 'string' || name.length === 0) {
17
17
  throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
18
18
  }
19
19
  const unscopedName = stripNpmScope(name);
20
- const privateField = isRecord(parsed) ? parsed.private : undefined;
20
+ const privateField = isRecord(parsed) ? parsed['private'] : undefined;
21
21
  const isPublishable = privateField === undefined || privateField === false;
22
22
  return {
23
23
  dir,
@@ -6,8 +6,8 @@ export function detectPackageManager() {
6
6
  try {
7
7
  const content = readFileSync(packageJsonPath, 'utf8');
8
8
  const parsed = JSON.parse(content);
9
- if (isRecord(parsed) && typeof parsed.packageManager === 'string') {
10
- const [name, version] = parsed.packageManager.split('@', 2);
9
+ if (isRecord(parsed) && typeof parsed['packageManager'] === 'string') {
10
+ const [name, version] = parsed['packageManager'].split('@', 2);
11
11
  if (name === 'pnpm' || name === 'npm') {
12
12
  return name;
13
13
  }
@@ -23,7 +23,7 @@ export function detectUndeclaredTagPrefixes(knownPrefixes) {
23
23
  const match = CANDIDATE_TAG_PATTERN.exec(tag);
24
24
  if (match === null)
25
25
  continue;
26
- const prefix = match.groups?.prefix ?? '';
26
+ const prefix = match.groups?.['prefix'] ?? '';
27
27
  if (prefix === '' || known.has(prefix))
28
28
  continue;
29
29
  let tags = grouped.get(prefix);
@@ -20,7 +20,7 @@ export async function discoverWorkspaces() {
20
20
  if (!isRecord(parsed)) {
21
21
  return undefined;
22
22
  }
23
- const packagesField = parsed.packages;
23
+ const packagesField = parsed['packages'];
24
24
  if (!Array.isArray(packagesField)) {
25
25
  return undefined;
26
26
  }
@@ -24,7 +24,7 @@ function findLatestTag(tagPrefixes) {
24
24
  if (isNoTagError(error)) {
25
25
  return undefined;
26
26
  }
27
- throw new Error(`Failed to run 'git describe': ${errorMessage(error)}`);
27
+ throw new Error(`Failed to run 'git describe': ${errorMessage(error)}`, { cause: error });
28
28
  }
29
29
  }
30
30
  const RELEASE_COMMIT_PREFIX = 'release:';
@@ -59,7 +59,7 @@ export function getCommitsSinceTarget(tagPrefixes, paths) {
59
59
  }).trim();
60
60
  }
61
61
  catch (error) {
62
- throw new Error(`Failed to run 'git log' for range '${range}': ${errorMessage(error)}`);
62
+ throw new Error(`Failed to run 'git log' for range '${range}': ${errorMessage(error)}`, { cause: error });
63
63
  }
64
64
  if (logOutput === '') {
65
65
  return { tag, commits: [] };
@@ -1,2 +1,2 @@
1
- export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/\n (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
1
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
2
2
  export declare function showPrepareHelp(): void;
@@ -16,8 +16,8 @@ Options:
16
16
  --no-git-checks, -n Skip the clean-working-tree check
17
17
  --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
18
18
  rejected when a 'project' block is configured)
19
- --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
20
- (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
19
+ --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/
20
+ (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).
21
21
  Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
22
22
  --help, -h Show this help message
23
23
  `;
@@ -23,7 +23,7 @@ export function usesPnpm() {
23
23
  }
24
24
  const raw = readFileSync('package.json', 'utf8');
25
25
  const pkg = parseJsonRecord(raw);
26
- if (pkg !== undefined && typeof pkg.packageManager === 'string' && pkg.packageManager.startsWith('pnpm')) {
26
+ if (pkg !== undefined && typeof pkg['packageManager'] === 'string' && pkg['packageManager'].startsWith('pnpm')) {
27
27
  return { ok: true };
28
28
  }
29
29
  return {
@@ -7,7 +7,7 @@ export function detectRepoType() {
7
7
  if (existsSync('package.json')) {
8
8
  const raw = readFileSync('package.json', 'utf8');
9
9
  const pkg = parseJsonRecord(raw);
10
- if (pkg !== undefined && Array.isArray(pkg.workspaces)) {
10
+ if (pkg !== undefined && Array.isArray(pkg['workspaces'])) {
11
11
  return 'monorepo';
12
12
  }
13
13
  }
@@ -23,19 +23,19 @@ export function readRootPackageVersion() {
23
23
  contents = readFileSync(absolutePath, 'utf8');
24
24
  }
25
25
  catch (error) {
26
- throw new Error(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`);
26
+ throw new Error(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
27
27
  }
28
28
  let parsed;
29
29
  try {
30
30
  parsed = JSON.parse(contents);
31
31
  }
32
32
  catch (error) {
33
- throw new Error(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`);
33
+ throw new Error(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
34
34
  }
35
35
  if (!isRecord(parsed)) {
36
36
  return { exists: true, version: undefined };
37
37
  }
38
- return { exists: true, version: typeof parsed.version === 'string' ? parsed.version : undefined };
38
+ return { exists: true, version: typeof parsed['version'] === 'string' ? parsed['version'] : undefined };
39
39
  }
40
40
  export const CONFIG_FILE_PATH = '.config/release-kit.config.ts';
41
41
  export async function loadConfig() {
@@ -44,7 +44,7 @@ export async function loadConfig() {
44
44
  return undefined;
45
45
  }
46
46
  const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(absoluteConfigPath).href));
47
- const resolved = isRecord(imported) ? (imported.default ?? imported.config) : undefined;
47
+ const resolved = isRecord(imported) ? (imported['default'] ?? imported['config']) : undefined;
48
48
  if (resolved === undefined) {
49
49
  throw new Error('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
50
50
  }
@@ -177,7 +177,8 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
177
177
  for (const workspace of workspaces) {
178
178
  const owner = `ws:${workspace.dir}`;
179
179
  sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
180
- for (const identity of workspace.legacyIdentities ?? []) {
180
+ const legacyIdentities = workspace.legacyIdentities ?? [];
181
+ for (const identity of legacyIdentities) {
181
182
  sources.push({
182
183
  prefix: identity.tagPrefix,
183
184
  label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
@@ -185,7 +186,8 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
185
186
  });
186
187
  }
187
188
  }
188
- for (const [index, retired] of (retiredPackages ?? []).entries()) {
189
+ const retiredEntries = (retiredPackages ?? []).entries();
190
+ for (const [index, retired] of retiredEntries) {
189
191
  sources.push({
190
192
  prefix: retired.tagPrefix,
191
193
  label: `retiredPackages entry (name='${retired.name}')`,
@@ -34,14 +34,18 @@ function readPackageJson(filePath) {
34
34
  content = readFileSync(filePath, 'utf8');
35
35
  }
36
36
  catch (error) {
37
- throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
37
+ throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
38
+ cause: error,
39
+ });
38
40
  }
39
41
  let parsed;
40
42
  try {
41
43
  parsed = JSON.parse(content);
42
44
  }
43
45
  catch (error) {
44
- throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
46
+ throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
47
+ cause: error,
48
+ });
45
49
  }
46
50
  if (!isPackageJson(parsed)) {
47
51
  throw new Error(`No valid 'version' field found in ${filePath}`);
@@ -4,7 +4,7 @@ export function propagateBumps(directBumps, graph, currentVersions) {
4
4
  for (const [dir, entry] of directBumps) {
5
5
  result.set(dir, { ...entry });
6
6
  }
7
- const queue = [...directBumps.keys()];
7
+ const queue = directBumps.keys().toArray();
8
8
  const visited = new Set();
9
9
  while (queue.length > 0) {
10
10
  const dir = queue.shift();
@@ -472,7 +472,8 @@ function topologicalSort(releaseSet, graph) {
472
472
  break;
473
473
  }
474
474
  sorted.push(dir);
475
- for (const dependent of forwardEdges.get(dir) ?? []) {
475
+ const dependents = forwardEdges.get(dir) ?? [];
476
+ for (const dependent of dependents) {
476
477
  const newDegree = (inDegree.get(dependent) ?? 1) - 1;
477
478
  inDegree.set(dependent, newDegree);
478
479
  if (newDegree === 0) {
@@ -202,7 +202,8 @@ function formatChangelogFiles(lines, workspace, dryRun, indent = '') {
202
202
  }
203
203
  }
204
204
  function formatPreviewFiles(lines, release, dryRun, indent = '') {
205
- for (const file of release.previewFiles ?? []) {
205
+ const previewFiles = release.previewFiles ?? [];
206
+ for (const file of previewFiles) {
206
207
  if (dryRun) {
207
208
  lines.push(dim(`${indent} [dry-run] Would write ${file}`));
208
209
  }
@@ -10,7 +10,8 @@ import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
10
10
  export const LABELS_OUTPUT_PATH = '.github/labels.yaml';
11
11
  export function formatLabelsYaml(labels, presetHashes) {
12
12
  const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${CONFIG_FILE_PATH}`];
13
- for (const [name, hash] of [...presetHashes].sort(([a], [b]) => a.localeCompare(b))) {
13
+ const sortedPresetHashes = [...presetHashes].sort(([a], [b]) => a.localeCompare(b));
14
+ for (const [name, hash] of sortedPresetHashes) {
14
15
  headerLines.push(`# ${name} preset hash: ${hash}`);
15
16
  }
16
17
  const declared = labels.map((label) => ({ ...label, description: label.description ?? '' }));
@@ -50,7 +51,8 @@ export async function generateCommand({ check = false } = {}) {
50
51
  return 1;
51
52
  }
52
53
  const presetHashes = new Map();
53
- for (const presetName of repoLabels.extends ?? []) {
54
+ const presetNames = repoLabels.extends ?? [];
55
+ for (const presetName of presetNames) {
54
56
  presetHashes.set(presetName, hashPresetFile(presetName));
55
57
  }
56
58
  const content = formatLabelsYaml(labels, presetHashes);
@@ -23,7 +23,7 @@ export function loadPreset(presetName) {
23
23
  }
24
24
  catch (error) {
25
25
  const message = error instanceof Error ? error.message : String(error);
26
- throw new Error(`Failed to read preset "${presetName}": ${message}`);
26
+ throw new Error(`Failed to read preset "${presetName}": ${message}`, { cause: error });
27
27
  }
28
28
  const parsed = parse(content);
29
29
  if (!Array.isArray(parsed)) {
@@ -34,13 +34,13 @@ export function loadPreset(presetName) {
34
34
  if (!isRecord(entry)) {
35
35
  throw new Error(`Preset "${presetName}" contains an invalid label entry: ${JSON.stringify(entry)}`);
36
36
  }
37
- const { description } = entry;
38
- if (typeof entry.name !== 'string' ||
39
- typeof entry.color !== 'string' ||
37
+ const { color, description, name } = entry;
38
+ if (typeof name !== 'string' ||
39
+ typeof color !== 'string' ||
40
40
  (description !== undefined && typeof description !== 'string')) {
41
41
  throw new TypeError(`Preset "${presetName}" contains a label with invalid fields: ${JSON.stringify(entry)}`);
42
42
  }
43
- labels.push({ name: entry.name, color: entry.color, ...(description !== undefined && { description }) });
43
+ labels.push({ name, color, ...(description !== undefined && { description }) });
44
44
  }
45
45
  return labels;
46
46
  }
@@ -1,12 +1,14 @@
1
1
  import { loadPreset } from "./presets.js";
2
2
  export function resolveLabels(config) {
3
3
  const resolved = new Map();
4
- for (const presetName of config.extends ?? []) {
4
+ const presetNames = config.extends ?? [];
5
+ for (const presetName of presetNames) {
5
6
  for (const label of loadPreset(presetName)) {
6
7
  resolved.set(label.name, label);
7
8
  }
8
9
  }
9
- for (const [name, spec] of Object.entries(config.labels ?? {})) {
10
+ const labelEntries = Object.entries(config.labels ?? {});
11
+ for (const [name, spec] of labelEntries) {
10
12
  if (spec === null) {
11
13
  if (!resolved.has(name)) {
12
14
  throw new Error(`Label '${name}' is set to null (remove), but no preset in 'extends' defines it. Fix the name or delete the entry.`);
@@ -21,7 +23,7 @@ export function resolveLabels(config) {
21
23
  });
22
24
  }
23
25
  }
24
- return sortLabels([...resolved.values()]);
26
+ return sortLabels(resolved.values().toArray());
25
27
  }
26
28
  function sortLabels(labels) {
27
29
  return [...labels].sort((a, b) => a.name.localeCompare(b.name));
@@ -20,7 +20,7 @@ function extractLocalSchemaUrl(content) {
20
20
  if (!isRecord(parsed)) {
21
21
  return undefined;
22
22
  }
23
- const schema = parsed.$schema;
23
+ const schema = parsed['$schema'];
24
24
  return typeof schema === 'string' ? schema : undefined;
25
25
  }
26
26
  export async function syncWorkTypes(dependencies = {}) {
@@ -30,25 +30,25 @@ function preprocessDeprecatedKeys(raw) {
30
30
  return { cleaned: raw, deprecationErrors: [] };
31
31
  const errors = [];
32
32
  const cleaned = { ...raw };
33
- if (isRecord(cleaned.releaseNotes) && 'shouldCreateGithubRelease' in cleaned.releaseNotes) {
33
+ if (isRecord(cleaned['releaseNotes']) && Object.hasOwn(cleaned['releaseNotes'], 'shouldCreateGithubRelease')) {
34
34
  errors.push('releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow. Remove this field from your config; see README for the updated workflow.');
35
- const releaseNotesCopy = { ...cleaned.releaseNotes };
36
- delete releaseNotesCopy.shouldCreateGithubRelease;
37
- cleaned.releaseNotes = releaseNotesCopy;
35
+ const releaseNotesCopy = { ...cleaned['releaseNotes'] };
36
+ delete releaseNotesCopy['shouldCreateGithubRelease'];
37
+ cleaned['releaseNotes'] = releaseNotesCopy;
38
38
  }
39
- if (Array.isArray(cleaned.workspaces)) {
40
- cleaned.workspaces = cleaned.workspaces.map((ws, i) => {
39
+ if (Array.isArray(cleaned['workspaces'])) {
40
+ cleaned['workspaces'] = cleaned['workspaces'].map((ws, i) => {
41
41
  if (!isRecord(ws))
42
42
  return ws;
43
43
  const wsCopy = { ...ws };
44
- if ('tagPrefix' in wsCopy) {
45
- const dir = typeof wsCopy.dir === 'string' && wsCopy.dir !== '' ? wsCopy.dir : '<dir>';
44
+ if (Object.hasOwn(wsCopy, 'tagPrefix')) {
45
+ const dir = typeof wsCopy['dir'] === 'string' && wsCopy['dir'] !== '' ? wsCopy['dir'] : '<dir>';
46
46
  errors.push(`workspaces[${i}]: 'tagPrefix' is no longer supported; remove it to use the default '${dir}-v'`);
47
- delete wsCopy.tagPrefix;
47
+ delete wsCopy['tagPrefix'];
48
48
  }
49
- if ('legacyTagPrefixes' in wsCopy) {
49
+ if (Object.hasOwn(wsCopy, 'legacyTagPrefixes')) {
50
50
  errors.push(`workspaces[${i}]: 'legacyTagPrefixes' is no longer supported; use 'legacyIdentities: [{ name, tagPrefix }, ...]' instead`);
51
- delete wsCopy.legacyTagPrefixes;
51
+ delete wsCopy['legacyTagPrefixes'];
52
52
  }
53
53
  return wsCopy;
54
54
  });
@@ -11,7 +11,7 @@ export function errorMessage(error) {
11
11
  return error instanceof Error ? error.message : String(error);
12
12
  }
13
13
  export function buildFetchInit() {
14
- const token = process.env.GITHUB_TOKEN;
14
+ const token = process.env['GITHUB_TOKEN'];
15
15
  if (token === undefined || token === '') {
16
16
  return undefined;
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "10.2.2",
3
+ "version": "10.3.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
@@ -44,11 +44,11 @@
44
44
  "semver": "7.8.5",
45
45
  "yaml": "2.9.0",
46
46
  "zod": "4.4.3",
47
- "@williamthorsen/nmr-core": "0.9.2"
47
+ "@williamthorsen/nmr-core": "0.9.3"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/semver": "7.7.1",
51
- "readyup": "0.23.0",
51
+ "readyup": "0.24.0",
52
52
  "smol-toml": "1.7.1"
53
53
  },
54
54
  "engines": {