@entro314labs/release-kit 2.3.0 → 2.3.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 +32 -23
  2. package/package.json +1 -1
  3. package/release.mjs +55 -2
package/README.md CHANGED
@@ -94,7 +94,7 @@ release-kit minor
94
94
  No global state to drift, no install step, and the version is explicit in the command.
95
95
 
96
96
  ```sh
97
- npx @entro314labs/release-kit@2.1.0 minor --yes
97
+ npx @entro314labs/release-kit@2.3.0 minor --yes
98
98
  ```
99
99
 
100
100
  ### Vendored — no registry at release time
@@ -116,7 +116,7 @@ npx @entro314labs/release-kit --sync . # writes scripts/release.mjs
116
116
  machine you do not want to install anything on.
117
117
 
118
118
  ```sh
119
- curl -fsSL https://raw.githubusercontent.com/entro314-labs/release-kit/v2.1.0/release.mjs \
119
+ curl -fsSL https://raw.githubusercontent.com/entro314-labs/release-kit/v2.3.0/release.mjs \
120
120
  | node - minor --yes
121
121
  ```
122
122
 
@@ -172,16 +172,20 @@ Prerelease bumps need `--preid` unless the current version already carries one t
172
172
 
173
173
  ### Flags
174
174
 
175
- | Flag | Effect |
176
- | ------------------- | -------------------------------------------------------------------------- |
177
- | `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
178
- | `--yes`, `-y` | Skip the confirmation prompt. |
179
- | `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
180
- | `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
181
- | `--only <steps>` | Run only these steps, comma-separated. |
182
- | `--skip <steps>` | Run every step except these. |
183
- | `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
184
- | `--help`, `-h` | Full flag list. |
175
+ | Flag | Effect |
176
+ | ---------------------------- | -------------------------------------------------------------------------- |
177
+ | `--only <steps>` | Run only these steps, comma-separated. |
178
+ | `--skip <steps>` | Run every step except these. |
179
+ | `--commit` | Add the opt-in `commit` step: commit a dirty tree with a drafted message. |
180
+ | `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
181
+ | `--yes`, `-y` | Skip the confirmation prompt. |
182
+ | `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
183
+ | `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
184
+ | `--assistant <name>` | Drafting CLI: `auto`, `none`, `claude`, `codex`. |
185
+ | `--assistant-model <name>` | Model the assistant runs with. |
186
+ | `--assistant-effort <level>` | Reasoning effort the assistant runs with. |
187
+ | `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
188
+ | `--help`, `-h` | Full flag list. |
185
189
 
186
190
  ## 🧩 Steps
187
191
 
@@ -256,17 +260,18 @@ falling through to `latest` would put a prerelease on the stable line where ever
256
260
  Every check runs and every failure is reported before it aborts once with the whole list,
257
261
  rather than stopping at the first problem.
258
262
 
259
- - The target version is greater than the current one
260
- - Working tree is clean
261
- - On the configured branch
263
+ - The target version is greater than the current one — and for `auto`, which bump the
264
+ commits imply and why
265
+ - Working tree is clean, or listed for commit when the `commit` step runs
266
+ - On the configured branch, and not on a detached HEAD
262
267
  - The remote exists, is reachable, and the branch is not behind it
263
268
  - The tag is free — or already exists at `HEAD`, in which case it is reused
264
269
  - `gh` is installed and authenticated
265
- - Commit and tag signing can actually sign, when `commit.gpgsign` or `tag.gpgsign` is on
266
- - The publishing CLI is authenticated, and the version is not already on the registry
270
+ - Commit and tag signing can actually sign, and the key is one GitHub will accept
271
+ - The publishing CLI is authenticated, and the version is not already published
267
272
  - Configured release assets exist
268
- - A changelog section for the version exists _(a warning, not a failure — it falls back
269
- to generated notes)_
273
+ - A shallow clone is reported, since it truncates the history notes come from _(warning)_
274
+ - A changelog section for the version exists _(warning — it falls back to generated notes)_
270
275
 
271
276
  Under `--dry-run` the failures are reported and then the remaining steps are shown anyway,
272
277
  so you can see the whole plan without fixing the blockers first.
@@ -348,12 +353,16 @@ rather than being silently ignored.
348
353
 
349
354
  | Key | Default | Meaning |
350
355
  | --------------- | ------------------------ | ------------------------------------------------------------ |
356
+ | `steps` | all but `commit` | Which steps run; the order is fixed |
351
357
  | `tagPrefix` | `"v"` | Prepended to the version to form the tag |
352
358
  | `branch` | `"main"` | The only branch a release may run from; `null` allows any |
353
359
  | `remote` | `"origin"` | Git remote to push to |
354
- | `changelog` | `"CHANGELOG.md"` | Changelog path; `null` disables changelog handling |
355
- | `versionFiles` | `[]` | Extra JSON files whose top-level `"version"` is kept in sync |
356
- | `publish` | `"npm publish --tag %d"` | Publish command; `null` skips publishing |
360
+ | `changelog` | `"CHANGELOG.md"` | Changelog path; `null` for a project without one |
361
+ | `versionFile` | detected | Where the version lives; `null` versions by tag alone |
362
+ | `versionFiles` | `[]` | Further files kept in sync; a path or `{ path, pattern }` |
363
+ | `publish` | `"npm publish --tag %d"` | Publish command; `null` means none is configured |
364
+ | `versioning` | `"conventional"` | How `auto` infers; or `always-patch` / `-minor` / `-major` |
365
+ | `assistant` | `null` | Drafting CLI: a name, `"auto"`, or `{ tool, model, effort }` |
357
366
  | `commitMessage` | `"chore(release): %t"` | Release commit subject |
358
367
  | `releaseTitle` | `"%t"` | GitHub release title |
359
368
  | `assets` | `[]` | Files attached to the GitHub release |
@@ -373,7 +382,7 @@ Non-interactive by default: the confirmation prompt is skipped when stdin is not
373
382
  with:
374
383
  fetch-depth: 0 # release notes and the last-tag lookup need real history
375
384
  - id: release
376
- run: npx @entro314labs/release-kit@2.1.0 minor --yes
385
+ run: npx @entro314labs/release-kit@2.3.0 minor --yes
377
386
  env:
378
387
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
379
388
  - run: echo "shipped ${{ steps.release.outputs.tag }} ${{ steps.release.outputs.release-url }}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entro314labs/release-kit",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Single-file, zero-dependency release mechanism for JS/TS/Node projects: version bump, changelog roll, commit, annotated tag, push, publish, GitHub release",
5
5
  "keywords": [
6
6
  "changelog",
package/release.mjs CHANGED
@@ -229,6 +229,22 @@ function read(command, args) {
229
229
  }).trim()
230
230
  }
231
231
 
232
+ /**
233
+ * Whether GitHub knows the key git signs with.
234
+ *
235
+ * Compares key material rather than fingerprints, so no extra process is spawned per key.
236
+ *
237
+ * @returns {boolean | null} null when it cannot be determined — gh missing, token without
238
+ * the scope to list signing keys, or no network. An unanswerable check is not a failure.
239
+ */
240
+ function signingKeyRegistered(keyPath) {
241
+ const local = readFileSync(keyPath, 'utf8').trim().split(/\s+/)[1]
242
+ if (!local) return null
243
+ const listed = tryRead('gh', ['api', 'user/ssh_signing_keys', '--jq', '.[].key'])
244
+ if (listed === null) return null
245
+ return listed.split('\n').some((key) => key.trim().split(/\s+/)[1] === local)
246
+ }
247
+
232
248
  /** Read-only command → trimmed stdout, or null when it exits non-zero. */
233
249
  function tryRead(command, args) {
234
250
  try {
@@ -740,9 +756,18 @@ function changelogSection(text, version) {
740
756
  * @returns {string | null} the updated document, or null when there is nothing to roll
741
757
  */
742
758
  function rollUnreleased(text, version, date) {
759
+ // A heading for this version already exists — possibly with an empty body, which
760
+ // `changelogSection` reports as absent. Rolling again would duplicate the heading.
761
+ if (new RegExp(`^##\\s+\\[?v?${escapeRe(version)}\\]?(?![\\w.-])`, 'm').test(text)) return null
762
+
743
763
  const heading = /^##\s+\[?Unreleased\]?[^\n]*$/im
744
764
  const match = heading.exec(text)
745
765
  if (!match) return null
766
+
767
+ // An empty [Unreleased] has nothing to promote; rolling it produces an empty section.
768
+ const rest = text.slice(match.index + match[0].length)
769
+ const body = (/^## /m.exec(rest) ? rest.slice(0, /^## /m.exec(rest).index) : rest).trim()
770
+ if (!body) return null
746
771
  const released = `## [Unreleased]\n\n## [${version}] - ${date}`
747
772
  return text.slice(0, match.index) + released + text.slice(match.index + match[0].length)
748
773
  }
@@ -1289,9 +1314,9 @@ if (runs('commit') && !assistant) {
1289
1314
  .join(', ')
1290
1315
  ok(`assistant: ${assistantName}${detail ? ` (${detail})` : ''}`)
1291
1316
  if (assistantModel && !assistant.model)
1292
- warn(`${assistantName} takes no model flag — --model ignored`)
1317
+ warn(`${assistantName} takes no model flag — --assistant-model ignored`)
1293
1318
  if (assistantEffort && !assistant.effort)
1294
- warn(`${assistantName} takes no effort flag — --effort ignored`)
1319
+ warn(`${assistantName} takes no effort flag — --assistant-effort ignored`)
1295
1320
  }
1296
1321
 
1297
1322
  const branch = tryRead('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
@@ -1351,6 +1376,22 @@ if (signsSomething && signingKeys.length) {
1351
1376
  )
1352
1377
  } else {
1353
1378
  ok(`signing commits and tags (${format})`)
1379
+ // git signs happily with a key GitHub has never seen, which is how a repository fills
1380
+ // up with commits that are locally valid and permanently "Unverified". Only GitHub can
1381
+ // answer this, and only when the token carries the scope to list signing keys — so an
1382
+ // unanswerable check stays silent rather than guessing.
1383
+ if (format === 'ssh' && keyPath && existsSync(keyPath) && runs('release')) {
1384
+ const registered = signingKeyRegistered(keyPath)
1385
+ if (registered === false) {
1386
+ warn(
1387
+ 'this signing key is not registered with GitHub, so the commits will show as ' +
1388
+ 'Unverified.\n Add it: gh ssh-key add ' +
1389
+ `${signingKey} --type signing`,
1390
+ )
1391
+ } else if (registered === true) {
1392
+ ok('signing key is registered with GitHub')
1393
+ }
1394
+ }
1354
1395
  }
1355
1396
  }
1356
1397
 
@@ -1483,6 +1524,18 @@ for (const asset of config.assets) {
1483
1524
  else fail(`asset ${asset} does not exist`)
1484
1525
  }
1485
1526
 
1527
+ // Reusing a tag is the resume path, and a resume writes nothing. If this run would still
1528
+ // produce a commit, that commit moves HEAD past the tag and the release ends up tagged at
1529
+ // the wrong revision — which is silent until someone checks out the tag.
1530
+ if (taggedCommit && runs('tag') && (bumping || rolledChangelog)) {
1531
+ fail(
1532
+ `tag ${tag} already exists at HEAD, but this run would still commit ` +
1533
+ `${[bumping && 'a version bump', rolledChangelog && 'a changelog entry'].filter(Boolean).join(' and ')}.\n` +
1534
+ ' That commit would leave the tag behind HEAD. Release a new version, or use ' +
1535
+ '--only with the steps that remain.',
1536
+ )
1537
+ }
1538
+
1486
1539
  if (problems.length) {
1487
1540
  const summary = `${problems.length} preflight check(s) failed:\n - ${problems.join('\n - ')}`
1488
1541
  if (!dryRun) abort(summary)