@entro314labs/release-kit 2.9.3 → 2.10.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.
package/README.md CHANGED
@@ -97,7 +97,7 @@ release-kit minor
97
97
  No global state to drift, no install step, and the version is explicit in the command.
98
98
 
99
99
  ```sh
100
- npx @entro314labs/release-kit@2.3.0 minor --yes
100
+ npx @entro314labs/release-kit@2.9.4 minor --yes
101
101
  ```
102
102
 
103
103
  ### Vendored — no registry at release time
@@ -164,7 +164,8 @@ footer) is a major, a `feat:` is a minor, anything else is a patch. Below `1.0.0
164
164
  softened — a breaking change bumps the minor rather than jumping to `1.0.0`. A commit body
165
165
  containing `Release-As: 2.0.0` pins the version outright. It always prints what it inferred
166
166
  and why before doing anything. Set `"versioning"` to `always-patch`, `always-minor` or
167
- `always-major` to never infer.
167
+ `always-major` to never infer. A `Notes:` line in a body sets what the release notes say
168
+ about that commit — see [Release notes](#release-notes).
168
169
 
169
170
  | Target | From `1.2.3` | From `2.0.0-beta.1` |
170
171
  | ------------------------------------ | ------------------------------------------------ | ------------------- |
@@ -185,20 +186,22 @@ Prerelease bumps need `--preid` unless the current version already carries one t
185
186
 
186
187
  ### Flags
187
188
 
188
- | Flag | Effect |
189
- | ---------------------------- | -------------------------------------------------------------------------- |
190
- | `--only <steps>` | Run only these steps, comma-separated. |
191
- | `--skip <steps>` | Run every step except these. |
192
- | `--commit` | Force the `commit` step on when a `steps` config removed it. |
193
- | `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
194
- | `--yes`, `-y` | Skip the confirmation prompt. |
195
- | `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
196
- | `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
197
- | `--assistant <name>` | Drafting CLI: `auto`, `none`, `claude`, `codex`. |
198
- | `--assistant-model <name>` | Model the assistant runs with. |
199
- | `--assistant-effort <level>` | Reasoning effort the assistant runs with. |
200
- | `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
201
- | `--help`, `-h` | Full flag list. |
189
+ | Flag | Effect |
190
+ | ---------------------------- | ----------------------------------------------------------------------------- |
191
+ | `--only <steps>` | Run only these steps, comma-separated. |
192
+ | `--skip <steps>` | Run every step except these. |
193
+ | `--commit` | Force the `commit` step on when a `steps` config removed it. |
194
+ | `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
195
+ | `--yes`, `-y` | Skip the confirmation prompt. |
196
+ | `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
197
+ | `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
198
+ | `--notes <source>` | Where notes come from: `auto`, `changelog`, `assistant`, `commits`, `github`. |
199
+ | `--notes-file <path>` | Write the resolved notes to a file for the next tool — see `notesFile`. |
200
+ | `--assistant <name>` | Drafting CLI: `auto`, `none`, `claude`, `codex`. |
201
+ | `--assistant-model <name>` | Model the assistant runs with. |
202
+ | `--assistant-effort <level>` | Reasoning effort the assistant runs with. |
203
+ | `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
204
+ | `--help`, `-h` | Full flag list. |
202
205
 
203
206
  ### Linting commits
204
207
 
@@ -225,7 +228,7 @@ commit released is the **pull request title**, which no hook ever sees — check
225
228
  - name: Lint the pull request title
226
229
  env:
227
230
  TITLE: ${{ github.event.pull_request.title }}
228
- run: npx @entro314labs/release-kit@2.8.0 lint-commits --subject "$TITLE"
231
+ run: npx @entro314labs/release-kit@2.9.4 lint-commits --subject "$TITLE"
229
232
  ```
230
233
 
231
234
  Pass the title through `env`, never through `${{ }}` inside `run:` — a pull request title is
@@ -286,8 +289,11 @@ Notes resolve in this order:
286
289
  2. The `## [Unreleased]` section, if the version has no section of its own — this is the
287
290
  same content that step 2 above is about to promote.
288
291
  3. The commits grouped by Conventional Commit type — Features, Bug Fixes, Performance
289
- Improvements, Reverts, with breaking changes first and chores, CI and docs hidden. Each
290
- bullet links to its commit, and `closes #12` / `fixes #34` in a message becomes a link to
292
+ Improvements, Reverts, Dependencies, Documentation, Code Refactoring, Build System,
293
+ Continuous Integration, Tests, Styles, Miscellaneous Chores — with breaking changes
294
+ first. Every type is reported, since a changelog is a record; list the ones to leave out
295
+ in `hiddenTypes` — `["chore", "ci", "docs"]`, say, for a shorter list. A type outside
296
+ the table, such as `security:`, lands under Other Changes. Each bullet links to its commit, and `closes #12` / `fixes #34` in a message becomes a link to
291
297
  the issue. A `BREAKING CHANGE:` footer is used in place of the subject, since it explains
292
298
  the break. A commit reverted within the same release drops out along with its revert.
293
299
  A **New Contributors** section names anyone whose first commit to the repository is in
@@ -297,6 +303,20 @@ Notes resolve in this order:
297
303
  rather than something that requires an assistant.
298
304
  4. Otherwise GitHub generates them from the commits since the previous tag.
299
305
 
306
+ **A `Notes:` line in a commit body is the author's wording for the entry.** Commit-derived
307
+ notes use it in place of the subject (and in place of a `BREAKING CHANGE:` footer), and an
308
+ [assistant](#-assistant-optional) is handed it with the commit and told to keep it.
309
+ `Notes: no-notes` leaves the commit out of the notes entirely — both kinds — for work that
310
+ has to be a `fix:` or `feat:` for the version bump but is not news to anyone upgrading. The
311
+ bump itself is unaffected: the trailer decides what is said about a change, not whether it
312
+ counts.
313
+
314
+ ```text
315
+ fix(ui): raise the badge z-index above the progress bar
316
+
317
+ Notes: The pull request badge stays visible while a push is in progress
318
+ ```
319
+
300
320
  **Which tag the history is read from** is the highest version tag carrying the configured
301
321
  prefix that is reachable from `HEAD` — not the nearest tag. A repository carrying tags that
302
322
  are not releases (a rolling `latest-beta` marker, a `nightly`) is unaffected by them, and a
@@ -306,7 +326,11 @@ patch tagged on top of a later minor does not drag the baseline backwards.
306
326
  `2.0.0-rc.1` and `-rc.2` reads history from the last _stable_ tag, so the notes describe
307
327
  everything the release ships rather than the gap between the last two candidates — which
308
328
  is usually just the release commit. Releasing a candidate is unchanged: each one's notes
309
- say what changed in that candidate.
329
+ say what changed in that candidate. The candidates' own changelog sections are not a source
330
+ for the stable release: its notes are generated from those commits, so wording edited into
331
+ a `[2.0.0-rc.1]` section does not carry over. The sections stay in the file, preflight warns
332
+ when it finds them, and writing the `2.0.0` notes into `[Unreleased]` before releasing is
333
+ how hand-written wording reaches the stable release.
310
334
 
311
335
  `--notes <source>` forces one instead of walking that list: `changelog`, `assistant`,
312
336
  `commits`, or `github`. A named source that produces nothing is an error rather than a
@@ -350,6 +374,13 @@ rather than stopping at the first problem.
350
374
 
351
375
  - The target version is greater than the current one — and for `auto`, which bump the
352
376
  commits imply and why
377
+ - The version step will write the target, when the target differs from what the files say
378
+ — a tag naming one version over a manifest carrying another is refused, since
379
+ `npm publish` sends the manifest
380
+ - A relative bump is not counting from a version a dead run wrote and never committed
381
+ (the file says one thing on disk and another at `HEAD`), and not skipping past a release
382
+ tagged at `HEAD` that never reached the registry — both are refused with the command that
383
+ finishes the earlier release instead
353
384
  - Working tree is clean, or listed for commit when the `commit` step runs
354
385
  - On the configured branch, and not on a detached HEAD
355
386
  - The remote exists, is reachable, and the branch is not behind it
@@ -357,9 +388,13 @@ rather than stopping at the first problem.
357
388
  - `gh` is installed and authenticated
358
389
  - Commit and tag signing can actually sign, and the key is one GitHub will accept
359
390
  - The publishing CLI is authenticated, and the version is not already published
391
+ - A crate that `cargo publish` will upload packages cleanly (`cargo package`) and fits
392
+ crates.io's 10 MiB upload limit — see [below](#crates-are-packaged-in-preflight)
360
393
  - The previous release actually reached the registry — one that did not is either finished
361
394
  by this run or absorbed into it _(warning)_
362
395
  - Configured release assets exist
396
+ - With `"requireGreen": true`, HEAD is on the remote and GitHub reports it green — see
397
+ [below](#requiring-a-green-head)
363
398
  - The configured `verify` command passes — the project's own gate (tests, build) runs
364
399
  before anything mutates, instead of a `prepublishOnly` hook failing after the commit,
365
400
  tag and push
@@ -369,10 +404,60 @@ rather than stopping at the first problem.
369
404
  previous tag reachable it passes, without one it fails `auto` (the bump would be inferred
370
405
  from partial history) and warns otherwise
371
406
  - A changelog section for the version exists _(warning — it falls back to generated notes)_
407
+ - Notes drafted by an [assistant](#-assistant-optional) carry no entry it marked `[???]`
408
+ as unsure — refused under `--yes` or without a terminal, listed for review at the prompt
409
+ otherwise
372
410
 
373
411
  Under `--dry-run` the failures are reported and then the remaining steps are shown anyway,
374
412
  so you can see the whole plan without fixing the blockers first.
375
413
 
414
+ ### Crates are packaged in preflight
415
+
416
+ `cargo publish` packages and verifies the crate as its first act, so a file your
417
+ `include`/`exclude` left out, metadata crates.io refuses, a crate that does not build from
418
+ its own archive, or an archive over crates.io's 10 MiB limit used to surface only after the
419
+ release was tagged and pushed. When a `cargo publish` command is going to run, preflight
420
+ runs `cargo package` with the same arguments first — `-p`, `--workspace`,
421
+ `--manifest-path`, `--features` and `--no-verify` carry over, so a workspace packages each
422
+ crate it will publish — and refuses on a failure or an oversized `.crate`.
423
+
424
+ - It packages the tree before the version bump. That is enough because the bump writes
425
+ the version into `Cargo.toml` _and_ `Cargo.lock` together, so the tree `cargo publish`
426
+ sees differs only in the version number, which changes neither the file list nor the
427
+ size. A `Cargo.lock` left on the old version would dirty the tree after the push instead:
428
+ it is detected beside a `Cargo.toml` version source, and a configured `versionFiles` that
429
+ writes `Cargo.toml` without it is refused while a `cargo publish` is going to run.
430
+ - `--locked` is added when the repository has a `Cargo.lock`; `--allow-dirty` only when the
431
+ tree is dirty (which preflight reports on its own, or the `commit` step is about to fix).
432
+ - Like `cargo publish`, it compiles the crate, so preflight takes a build longer. Put
433
+ `--no-verify` on the publish command if you want neither to compile.
434
+ - A publish command that is not a plain `cargo publish …` — a pipeline, quoting, an
435
+ environment prefix — is left alone, since what it uploads cannot be read off it.
436
+ - The 10 MiB is crates.io's default; it can raise the limit for a single crate on request.
437
+
438
+ ### Requiring a green HEAD
439
+
440
+ `verify` runs your gate locally. `"requireGreen": true` asks GitHub instead, before anything
441
+ is bumped: the release is refused unless the commit being released is on the remote (a
442
+ local commit has no checks, so HEAD must not be ahead of `origin/<branch>`) and its checks
443
+ passed. A dirty tree the `commit` step would fold in is refused too — CI never saw it.
444
+
445
+ - **Failed** — any check run concluding `failure`, `cancelled`, `timed_out`,
446
+ `action_required` or `stale`, or a commit status of `failure` or `error`. The failing
447
+ checks are named.
448
+ - **Not finished** — any check run still queued or in progress, or a status still
449
+ `pending`: wait for it and re-run.
450
+ - **Nothing checked** — no check run or status reporting `success`. Zero checks, or only
451
+ skipped ones, means nothing verified the commit, which is not the same as green.
452
+
453
+ Both of GitHub's systems are read — check runs (`commits/{sha}/check-runs`, what Actions
454
+ and GitHub Apps report) and commit statuses (`commits/{sha}/status`, what external CI such
455
+ as Buildkite or Jenkins still posts) — because either one alone can call a commit green
456
+ while the other has it red. It needs `gh`, authenticated, even with the `release` step off.
457
+ Run inside GitHub Actions, the check runs of the current workflow run are ignored: the job
458
+ doing the release is itself an unfinished check on that commit. Ordering against other jobs
459
+ in the same workflow is what `needs:` is for.
460
+
376
461
  ## ♻️ Recovering from a failed run
377
462
 
378
463
  Re-run the same command. Every step is idempotent:
@@ -393,6 +478,13 @@ last tag, and after a failed publish there are none — the tag it would read fr
393
478
  the dead run made. Rather than aborting with "no releasable commits", it finishes that
394
479
  release: same version, same tag, the steps that remain.
395
480
 
481
+ A relative bump is the one target that cannot be re-run as-is, because it counts from the
482
+ version the dead run already wrote: `minor` after a dead `minor` would release `1.2.0`
483
+ from the commit `v1.1.0` already tags. Preflight refuses both shapes of that — the bump
484
+ written but never committed, and the tag at `HEAD` that never reached the registry — and
485
+ names the command that finishes the earlier release: `release-kit 1.1.0`, or no target, or
486
+ `auto`.
487
+
396
488
  ### A release that was never published
397
489
 
398
490
  A tag is not a release. The tag and the push happen before the publish, so a publish that
@@ -563,6 +655,12 @@ too, and are refreshed by the tool that owns them rather than rewritten by patte
563
655
  version sits in more than one place and the formats change shape between tool versions.
564
656
  Each is scoped to its manifest, so a `uv.lock` for a component this release is not
565
657
  versioning stays out of the release commit, and a missing tool warns rather than aborting.
658
+
659
+ A `Cargo.lock` beside a detected `Cargo.toml` is kept in step without being listed, whether
660
+ `Cargo.toml` is the version source (a plain crate) or a companion manifest. A configured
661
+ `versionFiles` is never extended, so list the lockfile there yourself; when a `cargo publish`
662
+ is going to run and a written `Cargo.toml` would leave its `Cargo.lock` on the old version,
663
+ preflight refuses rather than letting `cargo publish` fail on the dirty tree after the push.
566
664
  `pnpm-lock.yaml` records no root version, so it never goes stale.
567
665
 
568
666
  ### Marking the line instead of writing a pattern
@@ -639,7 +737,9 @@ later — and the overlays that carry no `version` of their own are skipped rath
639
737
  failing the release.
640
738
 
641
739
  Stopping at `push` because the tag is what triggers the build pipeline — see
642
- [Libraries versus apps](#-libraries-versus-apps).
740
+ [Libraries versus apps](#-libraries-versus-apps). When release-kit itself runs in CI, the
741
+ push must not use `GITHUB_TOKEN`, or the tag triggers nothing — see
742
+ [Continuous integration](#continuous-integration).
643
743
 
644
744
  The project name comes from the manifest when there is one (`name` in `package.json`,
645
745
  `Cargo.toml` or `pyproject.toml`), and falls back to the repository directory.
@@ -666,23 +766,28 @@ execution is not wired up yet.
666
766
  `release.config.json`, beside `package.json`. Every key is optional; unknown keys abort
667
767
  rather than being silently ignored.
668
768
 
669
- | Key | Default | Meaning |
670
- | --------------- | ---------------------- | --------------------------------------------------------------------- |
671
- | `steps` | all but `commit` | Which steps run; the order is fixed |
672
- | `tagPrefix` | `"v"` | Prepended to the version to form the tag |
673
- | `branch` | `"main"` | The only branch a release may run from; `null` allows any |
674
- | `remote` | `"origin"` | Git remote to push to |
675
- | `changelog` | `"CHANGELOG.md"` | Changelog path; `null` for a project without one |
676
- | `versionFile` | detected | Where the version lives; `null` versions by tag alone |
677
- | `versionFiles` | detected | Further files kept in sync; a path or `{ path, pattern }` |
678
- | `publish` | detected | Publish command, or an array of them; `null` publishes nothing |
679
- | `versioning` | `"conventional"` | How `auto` infers; or `always-patch` / `-minor` / `-major` |
680
- | `verify` | `null` | Command run during preflight; non-zero aborts before anything mutates |
681
- | `hooks` | `{}` | Commands run between the steps — see [Hooks](#-hooks) |
682
- | `assistant` | `null` | Drafting CLI: a name, `"auto"`, or `{ tool, model, effort }` |
683
- | `commitMessage` | `"chore(release): %t"` | Release commit subject |
684
- | `releaseTitle` | `"%t"` | GitHub release title |
685
- | `assets` | `[]` | Files attached to the GitHub release |
769
+ | Key | Default | Meaning |
770
+ | --------------- | ---------------------- | ----------------------------------------------------------------------- |
771
+ | `steps` | all seven | Which steps run; the order is fixed. `commit` no-ops on a clean tree |
772
+ | `tagPrefix` | `"v"` | Prepended to the version to form the tag |
773
+ | `branch` | `"main"` | The only branch a release may run from; `null` allows any |
774
+ | `remote` | `"origin"` | Git remote to push to |
775
+ | `changelog` | `"CHANGELOG.md"` | Changelog path; `null` for a project without one |
776
+ | `versionFile` | detected | Where the version lives; `null` versions by tag alone |
777
+ | `versionFiles` | detected | Further files kept in sync; a path or `{ path, pattern }` |
778
+ | `publish` | detected | Publish command, or an array of them; `null` publishes nothing |
779
+ | `versioning` | `"conventional"` | How `auto` infers; or `always-patch` / `-minor` / `-major` |
780
+ | `verify` | `null` | Command run during preflight; non-zero aborts before anything mutates |
781
+ | `requireGreen` | `false` | Refuse a HEAD that is not pushed and green on GitHub |
782
+ | `hooks` | `{}` | Commands run between the steps — see [Hooks](#-hooks) |
783
+ | `assistant` | `null` | Drafting CLI: a name, `"auto"`, or `{ tool, model, effort }` |
784
+ | `commitMessage` | `"chore(release): %t"` | Release commit subject |
785
+ | `releaseTitle` | `"%t"` | GitHub release title |
786
+ | `assets` | `[]` | Files attached to the GitHub release |
787
+ | `notes` | `"auto"` | Notes source; or force `changelog` / `assistant` / `commits` / `github` |
788
+ | `notesFile` | `null` | Write the resolved notes here for the tool that runs next |
789
+ | `hiddenTypes` | `[]` | Commit types left out of commit-derived notes |
790
+ | `ignoreCommits` | release, merge, wip… | Regexes for commits that are bookkeeping rather than change |
686
791
 
687
792
  Command and message strings expand four tokens: `%v` version, `%t` tag, `%n` package
688
793
  name, `%d` npm dist-tag. In the `publish` command line the substituted values are
@@ -695,13 +800,19 @@ Non-interactive by default: the confirmation prompt is skipped when stdin is not
695
800
  `gh` picks up `GITHUB_TOKEN` on its own. Pass `--yes` to be explicit.
696
801
 
697
802
  ```yaml
698
- - uses: actions/checkout@v5
803
+ - uses: actions/create-github-app-token@v3
804
+ id: app-token
805
+ with:
806
+ client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
807
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
808
+ - uses: actions/checkout@v7
699
809
  with:
700
810
  fetch-depth: 0 # release notes and the last-tag lookup need real history
811
+ token: ${{ steps.app-token.outputs.token }} # the push below is made with this token
701
812
  - id: release
702
- run: npx @entro314labs/release-kit@2.3.0 minor --yes
813
+ run: npx @entro314labs/release-kit@2.9.4 minor --yes
703
814
  env:
704
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
815
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
705
816
  - run: echo "shipped ${{ steps.release.outputs.tag }} ${{ steps.release.outputs.release-url }}"
706
817
  ```
707
818
 
@@ -710,11 +821,71 @@ re-deriving it: `version`, `tag`, `name`, `dist-tag`, `steps`, `published`, `rel
710
821
  Nothing is written on a dry run, and an unwritable `$GITHUB_OUTPUT` never fails a release
711
822
  that already completed.
712
823
 
824
+ **A tag pushed with `GITHUB_TOKEN` triggers nothing.** GitHub deliberately does not start
825
+ workflows from events caused by the job's own `GITHUB_TOKEN`, so an `on: push: tags` build
826
+ workflow never runs for a tag release-kit pushed with it — the release is tagged and the
827
+ build that was supposed to follow it silently does not happen. The push uses whatever token
828
+ `actions/checkout` persisted, which is why the example gives checkout a
829
+ [GitHub App](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)
830
+ installation token (`actions/create-github-app-token`) — a fine-grained personal access
831
+ token with `contents: write` works the same way. Leave `persist-credentials` on for this
832
+ job: the push has nothing to authenticate with otherwise. The alternative that needs no
833
+ second identity is not to rely on the tag event at all: run the build in the same workflow
834
+ after the release job (`needs: release`), or call it as a reusable workflow
835
+ (`on: workflow_call`) with the tag from `steps.release.outputs.tag`. With `GITHUB_TOKEN`
836
+ alone, the example above still releases correctly; only the tag-triggered follow-up is lost.
837
+
713
838
  Two upstream habits make commit-derived notes trustworthy, and neither is release-kit's job:
714
839
 
715
840
  - **Gate the release on CI, and guard against forks.** Trigger on `workflow_run` after your
716
- check workflow succeeds, with `if: github.repository_owner == 'your-org'` so a fork never
717
- tries to release.
841
+ check workflow completes — but that event fires for _every_ completed run of it, including
842
+ pull request runs and failed ones, so the condition has to say which run may release.
843
+ `workflow_run` also checks out the default branch's latest commit, not the commit that
844
+ was checked, so check out `head_sha` explicitly:
845
+
846
+ ```yaml
847
+ on:
848
+ workflow_run:
849
+ workflows: [check]
850
+ types: [completed]
851
+ branches: [main]
852
+
853
+ jobs:
854
+ release:
855
+ # A push to main that passed, in this repository — not a fork's copy of the workflow.
856
+ if: >-
857
+ github.repository_owner == 'your-org' &&
858
+ github.event.workflow_run.event == 'push' &&
859
+ github.event.workflow_run.head_branch == 'main' &&
860
+ github.event.workflow_run.conclusion == 'success'
861
+ runs-on: ubuntu-latest
862
+ steps:
863
+ - uses: actions/create-github-app-token@v3
864
+ id: app-token
865
+ with:
866
+ client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
867
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
868
+ - uses: actions/checkout@v7
869
+ with:
870
+ ref: ${{ github.event.workflow_run.head_sha }}
871
+ fetch-depth: 0
872
+ token: ${{ steps.app-token.outputs.token }} # so the tag push triggers workflows
873
+ # Checking out a SHA leaves a detached HEAD, which release-kit refuses — there is no
874
+ # branch to push. Put main back at the commit CI checked; if main has moved on
875
+ # since, preflight refuses as "behind origin/main" instead of releasing it unchecked.
876
+ - run: git switch -C main
877
+ - run: npx @entro314labs/release-kit@2.9.4 auto --yes
878
+ env:
879
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
880
+ ```
881
+
882
+ [`requireGreen`](#requiring-a-green-head) is the same gate inside release-kit, for a
883
+ release run by hand or from a workflow that does not know which checks passed.
884
+
885
+ `branches: [main]` narrows the trigger to runs on `main`, but a pull request from a fork
886
+ whose branch is named `main` matches it too; `event == 'push'` is what rules pull request
887
+ runs out. The owner check stops a fork's own copy of the workflow from trying to release.
888
+
718
889
  - **Validate pull request titles.** A squash-merge takes its subject from the PR title, so
719
890
  that title becomes the commit the notes are built from. Check it with
720
891
  [`lint-commits`](#linting-commits), which uses this tool's own parser rather than a second
@@ -774,7 +945,13 @@ Two npm behaviours are handled automatically:
774
945
  at all.** In GitHub Actions with
775
946
  `id-token: write`, or GitLab CI/CircleCI with `NPM_ID_TOKEN`, `whoami` fails while
776
947
  `publish` succeeds. That environment is detected and the auth check is skipped, so a
777
- valid CI release is not aborted over a missing token it does not need.
948
+ valid CI release is not aborted over a missing token it does not need. That covers the
949
+ CLIs that exchange the OIDC token themselves — npm, pnpm, bun and `uv publish`. npm
950
+ only learned to in 11.5.1, and an older one fails the publish after the tag and push, so
951
+ under OIDC an older `npm` is a preflight failure (`npm install -g npm@latest` fixes it).
952
+ pnpm and bun document no minimum and are not checked. cargo is
953
+ not one of them: crates.io's trusted publishing goes through an action that turns the
954
+ token into `CARGO_REGISTRY_TOKEN`, so the cargo credential check still applies in CI.
778
955
 
779
956
  ### Examples
780
957
 
@@ -887,6 +1064,24 @@ downgrade, so a configured pipeline fails loudly; `"auto"` degrades quietly by d
887
1064
  annotation, and posted as the GitHub release body — the same "written once, lands in three
888
1065
  places" path a hand-written section takes.
889
1066
 
1067
+ The prompt asks for notes written for someone upgrading: internal chores (CI, formatting,
1068
+ version and dependency bumps) are left out, except a dependency that ships inside the
1069
+ product — a bundled runtime, a sidecar binary, an embedded engine — since users run that
1070
+ code; a fix is described as what works now rather than what was broken; and a commit's
1071
+ [`Notes:` line](#release-notes) is kept in the author's words.
1072
+
1073
+ **An entry the model is unsure of is flagged, not guessed.** The prompt lets it start a
1074
+ bullet with `[???]` when it cannot tell whether a change is user-facing or what it means for
1075
+ someone upgrading. Flagged entries are listed at the confirmation prompt, and answering `y`
1076
+ releases them as shown with the marker removed — your answer is the review. With `--yes`,
1077
+ or with no terminal to ask on (CI), there is nobody to review them, so preflight refuses
1078
+ before anything mutates: re-run in a terminal, or write the notes into `[Unreleased]` with
1079
+ the entries resolved, since a hand-written section wins over a draft. When a dirty tree
1080
+ defers drafting past the prompt, the check runs right after the working-tree commit instead
1081
+ and asks again (or, non-interactively, stops there with only that commit made). This applies
1082
+ to drafted notes only — commit-derived and hand-written notes are never checked for the
1083
+ marker.
1084
+
890
1085
  With `--commit`, notes are drafted _after_ that commit lands, so they describe the change it
891
1086
  just made. Merge, release, `WIP` and `fixup!`/`squash!` commits are excluded from the prompt.
892
1087
 
@@ -933,11 +1128,15 @@ release themselves, with the artifacts attached. That is their job. release-kit'
933
1128
  at the pushed tag:
934
1129
 
935
1130
  ```json
936
- { "steps": ["commit", "version", "changelog", "tag", "push"], "notesFile": "dist-notes.md" }
1131
+ { "steps": ["commit", "version", "changelog", "tag", "push"] }
937
1132
  ```
938
1133
 
939
1134
  Nothing after `push` — no `publish`, no `release`. The tag push is the handoff, and it is
940
- what triggers the build workflow:
1135
+ what triggers the build workflow — unless it was pushed with `GITHUB_TOKEN` from another
1136
+ workflow, which triggers nothing ([Continuous integration](#continuous-integration) has the
1137
+ fix). The notes travel in the annotated tag, which is the one
1138
+ thing that reaches a fresh checkout on another machine; the signature block a signed tag
1139
+ appends is stripped before the file is handed over:
941
1140
 
942
1141
  ```yaml
943
1142
  on:
@@ -947,8 +1146,12 @@ on:
947
1146
  jobs:
948
1147
  build:
949
1148
  steps:
950
- - uses: actions/checkout@v5
1149
+ - uses: actions/checkout@v7
951
1150
  with: { fetch-depth: 0 }
1151
+ - name: Read the release notes off the tag
1152
+ run: |
1153
+ git tag -l --format='%(contents)' "$GITHUB_REF_NAME" \
1154
+ | sed '/^-----BEGIN [A-Z ]*SIGNATURE-----$/,$d' > dist-notes.md
952
1155
  - run: goreleaser release --clean --release-notes dist-notes.md
953
1156
  ```
954
1157
 
@@ -968,10 +1171,11 @@ generated by different code from the same commits, and they drift.
968
1171
  release-kit has already created one for that tag, goreleaser fails. Exactly one of them
969
1172
  should own it, and it should be the one attaching the binaries.
970
1173
 
971
- `notesFile` exists for this handoff: goreleaser's `--release-notes` takes a file and skips
972
- its own changelog generation. The notes are also in the annotated tag, but reading them back
973
- with `git tag --format='%(contents)'` embeds the signature when tags are signed, which then
974
- appears in your published release notes. `notesFile` writes the text itself.
1174
+ `notesFile` is the same handoff for a build tool that runs next **on the same machine**:
1175
+ `release-kit auto && goreleaser release --release-notes dist-notes.md`. It writes the text
1176
+ itself, with no signature to strip. It is written after the release commit and is not part
1177
+ of it, so a workflow on another machine never sees it — that is what the tag read above is
1178
+ for.
975
1179
 
976
1180
  ### Both at once
977
1181
 
package/TRAIN.md CHANGED
@@ -12,7 +12,11 @@ readable, vendorable, zero dependencies.
12
12
  **Status: prototype.** Discovery, graph derivation, registry-aware change detection,
13
13
  cascade, planning, whole-train preflight, `seed-tags`, and the train summary work.
14
14
  Execution (running release-kit per package) is not implemented yet — `train` without
15
- `--dry-run` says so and exits.
15
+ `--dry-run` says so and exits. Three things the design below describes are not built
16
+ either, and the plan does not claim them: taking `packages` from `pnpm-workspace.yaml` /
17
+ `workspaces` when the config omits it (the config must list them today), and the two
18
+ per-package authentication checks in the preflight table (publish CLI and `gh`), which
19
+ each package's own release-kit run performs when execution lands.
16
20
 
17
21
  ```sh
18
22
  release-train graph # print the derived dependency graph and topo order
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entro314labs/release-kit",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
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",
@@ -49,11 +49,11 @@
49
49
  "release": "node release.mjs"
50
50
  },
51
51
  "devDependencies": {
52
- "oxfmt": "^0.65.0",
53
- "oxlint": "^1.80.0",
52
+ "oxfmt": "^0.67.0",
53
+ "oxlint": "^1.82.0",
54
54
  "semver": "^7.8.5"
55
55
  },
56
56
  "engines": {
57
- "node": ">=22"
57
+ "node": ">=24"
58
58
  }
59
59
  }