@entro314labs/release-kit 2.9.4 → 2.11.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 +221 -42
- package/TRAIN.md +78 -41
- package/package.json +3 -3
- package/release.mjs +671 -47
- package/train.mjs +328 -33
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.npmjs.com/package/@entro314labs/release-kit)
|
|
11
11
|
[](https://www.npmjs.com/package/@entro314labs/release-kit?activeTab=code)
|
|
12
12
|
[](#-requirements)
|
|
13
|
-
[](#-requirements)
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
@@ -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.
|
|
100
|
+
npx @entro314labs/release-kit@2.9.4 minor --yes
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
### Vendored — no registry at release time
|
|
@@ -127,7 +127,7 @@ Pin the URL to a tag, never `main`: piping an unpinned remote script into an int
|
|
|
127
127
|
means whatever is at that URL runs against your repository and your credentials. `--sync` is
|
|
128
128
|
the one thing that does not work this way — copying itself needs a file on disk.
|
|
129
129
|
|
|
130
|
-
> **All five paths run the same file and need Node
|
|
130
|
+
> **All five paths run the same file and need Node 24+.** That includes the Rust, Python and
|
|
131
131
|
> Go projects: `release-kit` is a Node program regardless of what it is releasing.
|
|
132
132
|
|
|
133
133
|
Zero-config works on the conventions below; add a [`release.config.json`](#️-configuration)
|
|
@@ -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,22 +186,23 @@ 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
|
-
| `--
|
|
194
|
-
| `--
|
|
195
|
-
| `--
|
|
196
|
-
| `--
|
|
197
|
-
| `--
|
|
198
|
-
| `--notes
|
|
199
|
-
| `--
|
|
200
|
-
| `--assistant
|
|
201
|
-
| `--assistant-
|
|
202
|
-
| `--
|
|
203
|
-
| `--
|
|
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
|
+
| `--package` | Release the package in this directory, one of several in the repository: its `release.config.json`, manifest, changelog and publish; commits and working tree limited to the directory; tagged `<name>@<version>` unless `tagPrefix` is set. release-train passes it. Without it, a nested package is refused. |
|
|
195
|
+
| `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
|
|
196
|
+
| `--yes`, `-y` | Skip the confirmation prompt. |
|
|
197
|
+
| `--preid <id>` | Prerelease identifier for a `pre*` bump: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
|
|
198
|
+
| `--dist-tag <name>` | Override the npm dist-tag. Always wins over the derived one. |
|
|
199
|
+
| `--notes <source>` | Where notes come from: `auto`, `changelog`, `assistant`, `commits`, `github`. |
|
|
200
|
+
| `--notes-file <path>` | Write the resolved notes to a file for the next tool — see `notesFile`. |
|
|
201
|
+
| `--assistant <name>` | Drafting CLI: `auto`, `none`, `claude`, `codex`. |
|
|
202
|
+
| `--assistant-model <name>` | Model the assistant runs with. |
|
|
203
|
+
| `--assistant-effort <level>` | Reasoning effort the assistant runs with. |
|
|
204
|
+
| `--sync <dir>...` | Copy this script into other projects and exit. Touches no git state. |
|
|
205
|
+
| `--help`, `-h` | Full flag list. |
|
|
204
206
|
|
|
205
207
|
### Linting commits
|
|
206
208
|
|
|
@@ -227,7 +229,7 @@ commit released is the **pull request title**, which no hook ever sees — check
|
|
|
227
229
|
- name: Lint the pull request title
|
|
228
230
|
env:
|
|
229
231
|
TITLE: ${{ github.event.pull_request.title }}
|
|
230
|
-
run: npx @entro314labs/release-kit@2.
|
|
232
|
+
run: npx @entro314labs/release-kit@2.9.4 lint-commits --subject "$TITLE"
|
|
231
233
|
```
|
|
232
234
|
|
|
233
235
|
Pass the title through `env`, never through `${{ }}` inside `run:` — a pull request title is
|
|
@@ -288,8 +290,11 @@ Notes resolve in this order:
|
|
|
288
290
|
2. The `## [Unreleased]` section, if the version has no section of its own — this is the
|
|
289
291
|
same content that step 2 above is about to promote.
|
|
290
292
|
3. The commits grouped by Conventional Commit type — Features, Bug Fixes, Performance
|
|
291
|
-
Improvements, Reverts,
|
|
292
|
-
|
|
293
|
+
Improvements, Reverts, Dependencies, Documentation, Code Refactoring, Build System,
|
|
294
|
+
Continuous Integration, Tests, Styles, Miscellaneous Chores — with breaking changes
|
|
295
|
+
first. Every type is reported, since a changelog is a record; list the ones to leave out
|
|
296
|
+
in `hiddenTypes` — `["chore", "ci", "docs"]`, say, for a shorter list. A type outside
|
|
297
|
+
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
|
|
293
298
|
the issue. A `BREAKING CHANGE:` footer is used in place of the subject, since it explains
|
|
294
299
|
the break. A commit reverted within the same release drops out along with its revert.
|
|
295
300
|
A **New Contributors** section names anyone whose first commit to the repository is in
|
|
@@ -299,6 +304,20 @@ Notes resolve in this order:
|
|
|
299
304
|
rather than something that requires an assistant.
|
|
300
305
|
4. Otherwise GitHub generates them from the commits since the previous tag.
|
|
301
306
|
|
|
307
|
+
**A `Notes:` line in a commit body is the author's wording for the entry.** Commit-derived
|
|
308
|
+
notes use it in place of the subject (and in place of a `BREAKING CHANGE:` footer), and an
|
|
309
|
+
[assistant](#-assistant-optional) is handed it with the commit and told to keep it.
|
|
310
|
+
`Notes: no-notes` leaves the commit out of the notes entirely — both kinds — for work that
|
|
311
|
+
has to be a `fix:` or `feat:` for the version bump but is not news to anyone upgrading. The
|
|
312
|
+
bump itself is unaffected: the trailer decides what is said about a change, not whether it
|
|
313
|
+
counts.
|
|
314
|
+
|
|
315
|
+
```text
|
|
316
|
+
fix(ui): raise the badge z-index above the progress bar
|
|
317
|
+
|
|
318
|
+
Notes: The pull request badge stays visible while a push is in progress
|
|
319
|
+
```
|
|
320
|
+
|
|
302
321
|
**Which tag the history is read from** is the highest version tag carrying the configured
|
|
303
322
|
prefix that is reachable from `HEAD` — not the nearest tag. A repository carrying tags that
|
|
304
323
|
are not releases (a rolling `latest-beta` marker, a `nightly`) is unaffected by them, and a
|
|
@@ -308,7 +327,11 @@ patch tagged on top of a later minor does not drag the baseline backwards.
|
|
|
308
327
|
`2.0.0-rc.1` and `-rc.2` reads history from the last _stable_ tag, so the notes describe
|
|
309
328
|
everything the release ships rather than the gap between the last two candidates — which
|
|
310
329
|
is usually just the release commit. Releasing a candidate is unchanged: each one's notes
|
|
311
|
-
say what changed in that candidate.
|
|
330
|
+
say what changed in that candidate. The candidates' own changelog sections are not a source
|
|
331
|
+
for the stable release: its notes are generated from those commits, so wording edited into
|
|
332
|
+
a `[2.0.0-rc.1]` section does not carry over. The sections stay in the file, preflight warns
|
|
333
|
+
when it finds them, and writing the `2.0.0` notes into `[Unreleased]` before releasing is
|
|
334
|
+
how hand-written wording reaches the stable release.
|
|
312
335
|
|
|
313
336
|
`--notes <source>` forces one instead of walking that list: `changelog`, `assistant`,
|
|
314
337
|
`commits`, or `github`. A named source that produces nothing is an error rather than a
|
|
@@ -366,9 +389,13 @@ rather than stopping at the first problem.
|
|
|
366
389
|
- `gh` is installed and authenticated
|
|
367
390
|
- Commit and tag signing can actually sign, and the key is one GitHub will accept
|
|
368
391
|
- The publishing CLI is authenticated, and the version is not already published
|
|
392
|
+
- A crate that `cargo publish` will upload packages cleanly (`cargo package`) and fits
|
|
393
|
+
crates.io's 10 MiB upload limit — see [below](#crates-are-packaged-in-preflight)
|
|
369
394
|
- The previous release actually reached the registry — one that did not is either finished
|
|
370
395
|
by this run or absorbed into it _(warning)_
|
|
371
396
|
- Configured release assets exist
|
|
397
|
+
- With `"requireGreen": true`, HEAD is on the remote and GitHub reports it green — see
|
|
398
|
+
[below](#requiring-a-green-head)
|
|
372
399
|
- The configured `verify` command passes — the project's own gate (tests, build) runs
|
|
373
400
|
before anything mutates, instead of a `prepublishOnly` hook failing after the commit,
|
|
374
401
|
tag and push
|
|
@@ -378,10 +405,60 @@ rather than stopping at the first problem.
|
|
|
378
405
|
previous tag reachable it passes, without one it fails `auto` (the bump would be inferred
|
|
379
406
|
from partial history) and warns otherwise
|
|
380
407
|
- A changelog section for the version exists _(warning — it falls back to generated notes)_
|
|
408
|
+
- Notes drafted by an [assistant](#-assistant-optional) carry no entry it marked `[???]`
|
|
409
|
+
as unsure — refused under `--yes` or without a terminal, listed for review at the prompt
|
|
410
|
+
otherwise
|
|
381
411
|
|
|
382
412
|
Under `--dry-run` the failures are reported and then the remaining steps are shown anyway,
|
|
383
413
|
so you can see the whole plan without fixing the blockers first.
|
|
384
414
|
|
|
415
|
+
### Crates are packaged in preflight
|
|
416
|
+
|
|
417
|
+
`cargo publish` packages and verifies the crate as its first act, so a file your
|
|
418
|
+
`include`/`exclude` left out, metadata crates.io refuses, a crate that does not build from
|
|
419
|
+
its own archive, or an archive over crates.io's 10 MiB limit used to surface only after the
|
|
420
|
+
release was tagged and pushed. When a `cargo publish` command is going to run, preflight
|
|
421
|
+
runs `cargo package` with the same arguments first — `-p`, `--workspace`,
|
|
422
|
+
`--manifest-path`, `--features` and `--no-verify` carry over, so a workspace packages each
|
|
423
|
+
crate it will publish — and refuses on a failure or an oversized `.crate`.
|
|
424
|
+
|
|
425
|
+
- It packages the tree before the version bump. That is enough because the bump writes
|
|
426
|
+
the version into `Cargo.toml` _and_ `Cargo.lock` together, so the tree `cargo publish`
|
|
427
|
+
sees differs only in the version number, which changes neither the file list nor the
|
|
428
|
+
size. A `Cargo.lock` left on the old version would dirty the tree after the push instead:
|
|
429
|
+
it is detected beside a `Cargo.toml` version source, and a configured `versionFiles` that
|
|
430
|
+
writes `Cargo.toml` without it is refused while a `cargo publish` is going to run.
|
|
431
|
+
- `--locked` is added when the repository has a `Cargo.lock`; `--allow-dirty` only when the
|
|
432
|
+
tree is dirty (which preflight reports on its own, or the `commit` step is about to fix).
|
|
433
|
+
- Like `cargo publish`, it compiles the crate, so preflight takes a build longer. Put
|
|
434
|
+
`--no-verify` on the publish command if you want neither to compile.
|
|
435
|
+
- A publish command that is not a plain `cargo publish …` — a pipeline, quoting, an
|
|
436
|
+
environment prefix — is left alone, since what it uploads cannot be read off it.
|
|
437
|
+
- The 10 MiB is crates.io's default; it can raise the limit for a single crate on request.
|
|
438
|
+
|
|
439
|
+
### Requiring a green HEAD
|
|
440
|
+
|
|
441
|
+
`verify` runs your gate locally. `"requireGreen": true` asks GitHub instead, before anything
|
|
442
|
+
is bumped: the release is refused unless the commit being released is on the remote (a
|
|
443
|
+
local commit has no checks, so HEAD must not be ahead of `origin/<branch>`) and its checks
|
|
444
|
+
passed. A dirty tree the `commit` step would fold in is refused too — CI never saw it.
|
|
445
|
+
|
|
446
|
+
- **Failed** — any check run concluding `failure`, `cancelled`, `timed_out`,
|
|
447
|
+
`action_required` or `stale`, or a commit status of `failure` or `error`. The failing
|
|
448
|
+
checks are named.
|
|
449
|
+
- **Not finished** — any check run still queued or in progress, or a status still
|
|
450
|
+
`pending`: wait for it and re-run.
|
|
451
|
+
- **Nothing checked** — no check run or status reporting `success`. Zero checks, or only
|
|
452
|
+
skipped ones, means nothing verified the commit, which is not the same as green.
|
|
453
|
+
|
|
454
|
+
Both of GitHub's systems are read — check runs (`commits/{sha}/check-runs`, what Actions
|
|
455
|
+
and GitHub Apps report) and commit statuses (`commits/{sha}/status`, what external CI such
|
|
456
|
+
as Buildkite or Jenkins still posts) — because either one alone can call a commit green
|
|
457
|
+
while the other has it red. It needs `gh`, authenticated, even with the `release` step off.
|
|
458
|
+
Run inside GitHub Actions, the check runs of the current workflow run are ignored: the job
|
|
459
|
+
doing the release is itself an unfinished check on that commit. Ordering against other jobs
|
|
460
|
+
in the same workflow is what `needs:` is for.
|
|
461
|
+
|
|
385
462
|
## ♻️ Recovering from a failed run
|
|
386
463
|
|
|
387
464
|
Re-run the same command. Every step is idempotent:
|
|
@@ -407,7 +484,9 @@ version the dead run already wrote: `minor` after a dead `minor` would release `
|
|
|
407
484
|
from the commit `v1.1.0` already tags. Preflight refuses both shapes of that — the bump
|
|
408
485
|
written but never committed, and the tag at `HEAD` that never reached the registry — and
|
|
409
486
|
names the command that finishes the earlier release: `release-kit 1.1.0`, or no target, or
|
|
410
|
-
`auto`.
|
|
487
|
+
`auto`. With `"publish": null` there is no registry to ask, so a tag at `HEAD` that the
|
|
488
|
+
remote does not have marks the unfinished release instead: a push that was refused is
|
|
489
|
+
finished by `auto` too, and a relative bump over it is refused the same way.
|
|
411
490
|
|
|
412
491
|
### A release that was never published
|
|
413
492
|
|
|
@@ -579,6 +658,12 @@ too, and are refreshed by the tool that owns them rather than rewritten by patte
|
|
|
579
658
|
version sits in more than one place and the formats change shape between tool versions.
|
|
580
659
|
Each is scoped to its manifest, so a `uv.lock` for a component this release is not
|
|
581
660
|
versioning stays out of the release commit, and a missing tool warns rather than aborting.
|
|
661
|
+
|
|
662
|
+
A `Cargo.lock` beside a detected `Cargo.toml` is kept in step without being listed, whether
|
|
663
|
+
`Cargo.toml` is the version source (a plain crate) or a companion manifest. A configured
|
|
664
|
+
`versionFiles` is never extended, so list the lockfile there yourself; when a `cargo publish`
|
|
665
|
+
is going to run and a written `Cargo.toml` would leave its `Cargo.lock` on the old version,
|
|
666
|
+
preflight refuses rather than letting `cargo publish` fail on the dirty tree after the push.
|
|
582
667
|
`pnpm-lock.yaml` records no root version, so it never goes stale.
|
|
583
668
|
|
|
584
669
|
### Marking the line instead of writing a pattern
|
|
@@ -655,7 +740,9 @@ later — and the overlays that carry no `version` of their own are skipped rath
|
|
|
655
740
|
failing the release.
|
|
656
741
|
|
|
657
742
|
Stopping at `push` because the tag is what triggers the build pipeline — see
|
|
658
|
-
[Libraries versus apps](#-libraries-versus-apps).
|
|
743
|
+
[Libraries versus apps](#-libraries-versus-apps). When release-kit itself runs in CI, the
|
|
744
|
+
push must not use `GITHUB_TOKEN`, or the tag triggers nothing — see
|
|
745
|
+
[Continuous integration](#continuous-integration).
|
|
659
746
|
|
|
660
747
|
The project name comes from the manifest when there is one (`name` in `package.json`,
|
|
661
748
|
`Cargo.toml` or `pyproject.toml`), and falls back to the repository directory.
|
|
@@ -667,6 +754,7 @@ release with the second bin in this package:
|
|
|
667
754
|
|
|
668
755
|
```sh
|
|
669
756
|
release-train --dry-run # plan + whole-train preflight, execute nothing
|
|
757
|
+
release-train # the same, then confirm and release (--yes without a terminal)
|
|
670
758
|
```
|
|
671
759
|
|
|
672
760
|
`train.mjs` derives the dependency graph and publish order from the package manifests
|
|
@@ -674,13 +762,14 @@ release-train --dry-run # plan + whole-train preflight, execute nothing
|
|
|
674
762
|
the per-package worker, rewrites internal ranges, and refuses the whole train before
|
|
675
763
|
anything mutates if any package would fail. A `train.config.json` declares only which
|
|
676
764
|
directories are members. Design, configuration and the full pipeline are in
|
|
677
|
-
[TRAIN.md](TRAIN.md).
|
|
678
|
-
|
|
765
|
+
[TRAIN.md](TRAIN.md). A member that shares its git repository with other members is
|
|
766
|
+
released with `--package`, so each keeps its own `<name>@<version>` tags and changelog.
|
|
679
767
|
|
|
680
768
|
## ⚙️ Configuration
|
|
681
769
|
|
|
682
|
-
`release.config.json`, beside `package.json`. Every key is optional; unknown keys
|
|
683
|
-
rather than being
|
|
770
|
+
`release.config.json`, beside `package.json`. Every key is optional; unknown keys, and known
|
|
771
|
+
keys holding the wrong type (a string where the table says array), abort rather than being
|
|
772
|
+
silently ignored.
|
|
684
773
|
|
|
685
774
|
| Key | Default | Meaning |
|
|
686
775
|
| --------------- | ---------------------- | ----------------------------------------------------------------------- |
|
|
@@ -694,6 +783,7 @@ rather than being silently ignored.
|
|
|
694
783
|
| `publish` | detected | Publish command, or an array of them; `null` publishes nothing |
|
|
695
784
|
| `versioning` | `"conventional"` | How `auto` infers; or `always-patch` / `-minor` / `-major` |
|
|
696
785
|
| `verify` | `null` | Command run during preflight; non-zero aborts before anything mutates |
|
|
786
|
+
| `requireGreen` | `false` | Refuse a HEAD that is not pushed and green on GitHub |
|
|
697
787
|
| `hooks` | `{}` | Commands run between the steps — see [Hooks](#-hooks) |
|
|
698
788
|
| `assistant` | `null` | Drafting CLI: a name, `"auto"`, or `{ tool, model, effort }` |
|
|
699
789
|
| `commitMessage` | `"chore(release): %t"` | Release commit subject |
|
|
@@ -715,13 +805,19 @@ Non-interactive by default: the confirmation prompt is skipped when stdin is not
|
|
|
715
805
|
`gh` picks up `GITHUB_TOKEN` on its own. Pass `--yes` to be explicit.
|
|
716
806
|
|
|
717
807
|
```yaml
|
|
718
|
-
- uses: actions/
|
|
808
|
+
- uses: actions/create-github-app-token@v3
|
|
809
|
+
id: app-token
|
|
810
|
+
with:
|
|
811
|
+
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
|
|
812
|
+
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
813
|
+
- uses: actions/checkout@v7
|
|
719
814
|
with:
|
|
720
815
|
fetch-depth: 0 # release notes and the last-tag lookup need real history
|
|
816
|
+
token: ${{ steps.app-token.outputs.token }} # the push below is made with this token
|
|
721
817
|
- id: release
|
|
722
|
-
run: npx @entro314labs/release-kit@2.
|
|
818
|
+
run: npx @entro314labs/release-kit@2.9.4 minor --yes
|
|
723
819
|
env:
|
|
724
|
-
GITHUB_TOKEN: ${{
|
|
820
|
+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
725
821
|
- run: echo "shipped ${{ steps.release.outputs.tag }} ${{ steps.release.outputs.release-url }}"
|
|
726
822
|
```
|
|
727
823
|
|
|
@@ -730,11 +826,71 @@ re-deriving it: `version`, `tag`, `name`, `dist-tag`, `steps`, `published`, `rel
|
|
|
730
826
|
Nothing is written on a dry run, and an unwritable `$GITHUB_OUTPUT` never fails a release
|
|
731
827
|
that already completed.
|
|
732
828
|
|
|
829
|
+
**A tag pushed with `GITHUB_TOKEN` triggers nothing.** GitHub deliberately does not start
|
|
830
|
+
workflows from events caused by the job's own `GITHUB_TOKEN`, so an `on: push: tags` build
|
|
831
|
+
workflow never runs for a tag release-kit pushed with it — the release is tagged and the
|
|
832
|
+
build that was supposed to follow it silently does not happen. The push uses whatever token
|
|
833
|
+
`actions/checkout` persisted, which is why the example gives checkout a
|
|
834
|
+
[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)
|
|
835
|
+
installation token (`actions/create-github-app-token`) — a fine-grained personal access
|
|
836
|
+
token with `contents: write` works the same way. Leave `persist-credentials` on for this
|
|
837
|
+
job: the push has nothing to authenticate with otherwise. The alternative that needs no
|
|
838
|
+
second identity is not to rely on the tag event at all: run the build in the same workflow
|
|
839
|
+
after the release job (`needs: release`), or call it as a reusable workflow
|
|
840
|
+
(`on: workflow_call`) with the tag from `steps.release.outputs.tag`. With `GITHUB_TOKEN`
|
|
841
|
+
alone, the example above still releases correctly; only the tag-triggered follow-up is lost.
|
|
842
|
+
|
|
733
843
|
Two upstream habits make commit-derived notes trustworthy, and neither is release-kit's job:
|
|
734
844
|
|
|
735
845
|
- **Gate the release on CI, and guard against forks.** Trigger on `workflow_run` after your
|
|
736
|
-
check workflow
|
|
737
|
-
|
|
846
|
+
check workflow completes — but that event fires for _every_ completed run of it, including
|
|
847
|
+
pull request runs and failed ones, so the condition has to say which run may release.
|
|
848
|
+
`workflow_run` also checks out the default branch's latest commit, not the commit that
|
|
849
|
+
was checked, so check out `head_sha` explicitly:
|
|
850
|
+
|
|
851
|
+
```yaml
|
|
852
|
+
on:
|
|
853
|
+
workflow_run:
|
|
854
|
+
workflows: [check]
|
|
855
|
+
types: [completed]
|
|
856
|
+
branches: [main]
|
|
857
|
+
|
|
858
|
+
jobs:
|
|
859
|
+
release:
|
|
860
|
+
# A push to main that passed, in this repository — not a fork's copy of the workflow.
|
|
861
|
+
if: >-
|
|
862
|
+
github.repository_owner == 'your-org' &&
|
|
863
|
+
github.event.workflow_run.event == 'push' &&
|
|
864
|
+
github.event.workflow_run.head_branch == 'main' &&
|
|
865
|
+
github.event.workflow_run.conclusion == 'success'
|
|
866
|
+
runs-on: ubuntu-latest
|
|
867
|
+
steps:
|
|
868
|
+
- uses: actions/create-github-app-token@v3
|
|
869
|
+
id: app-token
|
|
870
|
+
with:
|
|
871
|
+
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
|
|
872
|
+
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
873
|
+
- uses: actions/checkout@v7
|
|
874
|
+
with:
|
|
875
|
+
ref: ${{ github.event.workflow_run.head_sha }}
|
|
876
|
+
fetch-depth: 0
|
|
877
|
+
token: ${{ steps.app-token.outputs.token }} # so the tag push triggers workflows
|
|
878
|
+
# Checking out a SHA leaves a detached HEAD, which release-kit refuses — there is no
|
|
879
|
+
# branch to push. Put main back at the commit CI checked; if main has moved on
|
|
880
|
+
# since, preflight refuses as "behind origin/main" instead of releasing it unchecked.
|
|
881
|
+
- run: git switch -C main
|
|
882
|
+
- run: npx @entro314labs/release-kit@2.9.4 auto --yes
|
|
883
|
+
env:
|
|
884
|
+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
[`requireGreen`](#requiring-a-green-head) is the same gate inside release-kit, for a
|
|
888
|
+
release run by hand or from a workflow that does not know which checks passed.
|
|
889
|
+
|
|
890
|
+
`branches: [main]` narrows the trigger to runs on `main`, but a pull request from a fork
|
|
891
|
+
whose branch is named `main` matches it too; `event == 'push'` is what rules pull request
|
|
892
|
+
runs out. The owner check stops a fork's own copy of the workflow from trying to release.
|
|
893
|
+
|
|
738
894
|
- **Validate pull request titles.** A squash-merge takes its subject from the PR title, so
|
|
739
895
|
that title becomes the commit the notes are built from. Check it with
|
|
740
896
|
[`lint-commits`](#linting-commits), which uses this tool's own parser rather than a second
|
|
@@ -795,7 +951,10 @@ Two npm behaviours are handled automatically:
|
|
|
795
951
|
`id-token: write`, or GitLab CI/CircleCI with `NPM_ID_TOKEN`, `whoami` fails while
|
|
796
952
|
`publish` succeeds. That environment is detected and the auth check is skipped, so a
|
|
797
953
|
valid CI release is not aborted over a missing token it does not need. That covers the
|
|
798
|
-
CLIs that exchange the OIDC token themselves — npm, pnpm, bun and `uv publish`.
|
|
954
|
+
CLIs that exchange the OIDC token themselves — npm, pnpm, bun and `uv publish`. npm
|
|
955
|
+
only learned to in 11.5.1, and an older one fails the publish after the tag and push, so
|
|
956
|
+
under OIDC an older `npm` is a preflight failure (`npm install -g npm@latest` fixes it).
|
|
957
|
+
pnpm and bun document no minimum and are not checked. cargo is
|
|
799
958
|
not one of them: crates.io's trusted publishing goes through an action that turns the
|
|
800
959
|
token into `CARGO_REGISTRY_TOKEN`, so the cargo credential check still applies in CI.
|
|
801
960
|
|
|
@@ -910,6 +1069,24 @@ downgrade, so a configured pipeline fails loudly; `"auto"` degrades quietly by d
|
|
|
910
1069
|
annotation, and posted as the GitHub release body — the same "written once, lands in three
|
|
911
1070
|
places" path a hand-written section takes.
|
|
912
1071
|
|
|
1072
|
+
The prompt asks for notes written for someone upgrading: internal chores (CI, formatting,
|
|
1073
|
+
version and dependency bumps) are left out, except a dependency that ships inside the
|
|
1074
|
+
product — a bundled runtime, a sidecar binary, an embedded engine — since users run that
|
|
1075
|
+
code; a fix is described as what works now rather than what was broken; and a commit's
|
|
1076
|
+
[`Notes:` line](#release-notes) is kept in the author's words.
|
|
1077
|
+
|
|
1078
|
+
**An entry the model is unsure of is flagged, not guessed.** The prompt lets it start a
|
|
1079
|
+
bullet with `[???]` when it cannot tell whether a change is user-facing or what it means for
|
|
1080
|
+
someone upgrading. Flagged entries are listed at the confirmation prompt, and answering `y`
|
|
1081
|
+
releases them as shown with the marker removed — your answer is the review. With `--yes`,
|
|
1082
|
+
or with no terminal to ask on (CI), there is nobody to review them, so preflight refuses
|
|
1083
|
+
before anything mutates: re-run in a terminal, or write the notes into `[Unreleased]` with
|
|
1084
|
+
the entries resolved, since a hand-written section wins over a draft. When a dirty tree
|
|
1085
|
+
defers drafting past the prompt, the check runs right after the working-tree commit instead
|
|
1086
|
+
and asks again (or, non-interactively, stops there with only that commit made). This applies
|
|
1087
|
+
to drafted notes only — commit-derived and hand-written notes are never checked for the
|
|
1088
|
+
marker.
|
|
1089
|
+
|
|
913
1090
|
With `--commit`, notes are drafted _after_ that commit lands, so they describe the change it
|
|
914
1091
|
just made. Merge, release, `WIP` and `fixup!`/`squash!` commits are excluded from the prompt.
|
|
915
1092
|
|
|
@@ -960,7 +1137,9 @@ at the pushed tag:
|
|
|
960
1137
|
```
|
|
961
1138
|
|
|
962
1139
|
Nothing after `push` — no `publish`, no `release`. The tag push is the handoff, and it is
|
|
963
|
-
what triggers the build workflow
|
|
1140
|
+
what triggers the build workflow — unless it was pushed with `GITHUB_TOKEN` from another
|
|
1141
|
+
workflow, which triggers nothing ([Continuous integration](#continuous-integration) has the
|
|
1142
|
+
fix). The notes travel in the annotated tag, which is the one
|
|
964
1143
|
thing that reaches a fresh checkout on another machine; the signature block a signed tag
|
|
965
1144
|
appends is stripped before the file is handed over:
|
|
966
1145
|
|
|
@@ -972,7 +1151,7 @@ on:
|
|
|
972
1151
|
jobs:
|
|
973
1152
|
build:
|
|
974
1153
|
steps:
|
|
975
|
-
- uses: actions/checkout@
|
|
1154
|
+
- uses: actions/checkout@v7
|
|
976
1155
|
with: { fetch-depth: 0 }
|
|
977
1156
|
- name: Read the release notes off the tag
|
|
978
1157
|
run: |
|
|
@@ -1023,13 +1202,13 @@ npx @entro314labs/release-kit --sync ../project-a ../project-b
|
|
|
1023
1202
|
```
|
|
1024
1203
|
|
|
1025
1204
|
It reports `installed`, `updated`, or `already up to date` per target, creates `scripts/`
|
|
1026
|
-
if missing, skips
|
|
1027
|
-
`release` npm script. It runs before any git resolution, so it works from anywhere,
|
|
1205
|
+
if missing, skips a directory that does not exist, and warns when a target with a
|
|
1206
|
+
`package.json` lacks the `release` npm script. It runs before any git resolution, so it works from anywhere,
|
|
1028
1207
|
including a directory that is not a repository.
|
|
1029
1208
|
|
|
1030
1209
|
## 📋 Requirements
|
|
1031
1210
|
|
|
1032
|
-
- **Node
|
|
1211
|
+
- **Node 24+ — including for Rust, Python and Go projects.** `release-kit` is a Node
|
|
1033
1212
|
program whatever it releases; there is no standalone binary.
|
|
1034
1213
|
- `git`
|
|
1035
1214
|
- `gh`, authenticated — only when creating GitHub releases
|
|
@@ -1045,7 +1224,7 @@ with the reasoning — are in [ROADMAP.md](ROADMAP.md).
|
|
|
1045
1224
|
|
|
1046
1225
|
```sh
|
|
1047
1226
|
pnpm install
|
|
1048
|
-
pnpm test #
|
|
1227
|
+
pnpm test # node --test, no framework
|
|
1049
1228
|
pnpm check # format + lint + tests, the same gate CI runs
|
|
1050
1229
|
```
|
|
1051
1230
|
|
package/TRAIN.md
CHANGED
|
@@ -9,24 +9,27 @@ Ships in this package as `train.mjs` — a second self-contained, `node:*`-only
|
|
|
9
9
|
`release.mjs`, installed as the `release-train` bin. Same design contract as release-kit:
|
|
10
10
|
readable, vendorable, zero dependencies.
|
|
11
11
|
|
|
12
|
-
**Status
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
**Status.** Discovery, graph derivation, registry-aware change detection, cascade,
|
|
13
|
+
planning, whole-train preflight, execution, `seed-tags` and the train summary work, for
|
|
14
|
+
both topologies: a member alone in its repository releases exactly as standalone
|
|
15
|
+
release-kit would, and a member sharing its repository with others releases with
|
|
16
|
+
release-kit's `--package`. Not built yet, and not claimed:
|
|
17
|
+
|
|
18
|
+
- Taking `packages` from `pnpm-workspace.yaml` / `workspaces` when the config omits it (the
|
|
19
|
+
config must list them today).
|
|
20
|
+
- The two per-package authentication checks in the preflight table (publish CLI and `gh`)
|
|
21
|
+
run in each package's own release-kit preflight, at that package's turn — not up front.
|
|
20
22
|
|
|
21
23
|
```sh
|
|
24
|
+
release-train # plan + preflight, confirm, release (--yes: no prompt)
|
|
22
25
|
release-train graph # print the derived dependency graph and topo order
|
|
23
26
|
release-train --dry-run # full plan + whole-train preflight, execute nothing
|
|
24
|
-
release-train --
|
|
25
|
-
release-train
|
|
26
|
-
release-train seed-tags
|
|
27
|
-
release-train --summary <path>
|
|
28
|
-
release-train --offline
|
|
29
|
-
release-train --config <path>
|
|
27
|
+
release-train --all # every member, not just changed ones
|
|
28
|
+
release-train <id>... # these packages and their dependents
|
|
29
|
+
release-train seed-tags # baseline tags at each HEAD (--dry-run to preview)
|
|
30
|
+
release-train --summary <path> # write the train summary; --assistant drafts on top
|
|
31
|
+
release-train --offline --dry-run # no network: registry checks skipped, tags not pushed
|
|
32
|
+
release-train --config <path> # config elsewhere than ./train.config.json
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
## Problem
|
|
@@ -212,35 +215,67 @@ One failure anywhere aborts the entire train before any package releases. This i
|
|
|
212
215
|
whole safety story for the meta-workspace, where no transaction exists — so it is
|
|
213
216
|
deliberately strict: one dirty repo out of thirty blocks all thirty.
|
|
214
217
|
|
|
215
|
-
**Execute.**
|
|
218
|
+
**Execute.** After a confirmation prompt (`--yes` skips it, and is required without a
|
|
219
|
+
terminal), in topo order, per package:
|
|
216
220
|
|
|
217
221
|
1. Rewrite internal dependency ranges in this package's manifest to the versions its
|
|
218
|
-
dependencies just released, per `rangePolicy
|
|
222
|
+
dependencies just released, per `rangePolicy`, and commit that in the package's
|
|
223
|
+
repository as `chore(deps): move <name> <range>, …`. Skipped entirely for `workspace:`
|
|
219
224
|
ranges — the package manager rewrites those at publish time, which is the preferred
|
|
220
|
-
setup inside a workspace.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`
|
|
225
|
+
setup inside a workspace. The lockfile governing the package (in its directory, or up
|
|
226
|
+
to its repository root for a workspace) records those ranges, so it is refreshed by the
|
|
227
|
+
tool that owns it — `pnpm install --lockfile-only`, `npm install --package-lock-only`,
|
|
228
|
+
both with `--ignore-scripts` — and rides in the same commit; a stale one would fail the
|
|
229
|
+
repository's next frozen install. A `yarn.lock` or `bun.lock` the train would have to
|
|
230
|
+
rewrite is refused in preflight instead of being committed stale. It is a commit of its
|
|
231
|
+
own, not part of release-kit's release commit: release-kit refuses a dirty tree unless
|
|
232
|
+
its `commit` step runs, and that step would stage everything and draft a message. The
|
|
233
|
+
separate commit is deterministic, and it gives a cascade-only release — a package whose
|
|
234
|
+
only change is its dependency — a commit for its notes to describe.
|
|
235
|
+
2. Run release-kit — the `release.mjs` shipped beside `train.mjs`, one version for the
|
|
236
|
+
whole train — in the package directory with the planned version passed explicitly, so
|
|
237
|
+
release-kit releases exactly what the plan printed: bump, changelog, release commit,
|
|
238
|
+
tag, push, publish, GitHub release — whatever that package's `steps` say. A member
|
|
239
|
+
with `publish: false` runs with `--skip publish`; a Go member, which has no manifest
|
|
240
|
+
version, runs with `auto`; a member that shares its repository runs with `--package`
|
|
241
|
+
(below). `--assistant none` on the train is forwarded to every run.
|
|
224
242
|
3. If any member still to come depends on this package: poll the registry
|
|
225
|
-
(`npm view name@version`
|
|
226
|
-
visible or `registryWait.timeout` elapses. Registries have replication lag;
|
|
227
|
-
dependent that publishes or installs too early fails spuriously.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
243
|
+
(`npm view name@version version`) every `registryWait.interval` seconds until the new
|
|
244
|
+
version is visible or `registryWait.timeout` elapses. Registries have replication lag;
|
|
245
|
+
a dependent that publishes or installs too early fails spuriously.
|
|
246
|
+
|
|
247
|
+
The first failure stops the train and prints what was released, where it stopped, and
|
|
248
|
+
what never started.
|
|
249
|
+
|
|
250
|
+
**Several members in one repository.** release-kit on its own releases a whole
|
|
251
|
+
repository and refuses a nested package. `--package` releases the directory it runs in
|
|
252
|
+
instead: that directory's `release.config.json`, manifest, changelog, `verify` and publish;
|
|
253
|
+
`git log`, `git status` and the commit step limited to it (`-- .`); and tags
|
|
254
|
+
`<name>@<version>` unless the package's config sets `tagPrefix`. Each member gets its own
|
|
255
|
+
release commit, tag and push, one after another in topological order — release-kit's steps
|
|
256
|
+
unchanged, rather than batched into one commit, which would mean reordering them. A
|
|
257
|
+
member at the repository root that shares it with nested members reads every commit in the
|
|
258
|
+
repository, nested ones included. Inside a pnpm/npm workspace, internal ranges should be
|
|
259
|
+
`workspace:` and the member's publish command `pnpm publish` (or its equivalent), which is
|
|
260
|
+
what rewrites them into real ranges; a bare `npm publish` ships `workspace:` verbatim.
|
|
232
261
|
|
|
233
262
|
**Report.** What released at which version, what was skipped and why, and — on failure —
|
|
234
263
|
exactly which packages completed, so the resume story ("run it again") is verifiable.
|
|
235
264
|
|
|
236
265
|
## Failure and resume
|
|
237
266
|
|
|
238
|
-
| Died at
|
|
239
|
-
|
|
|
240
|
-
| Preflight
|
|
241
|
-
| Mid-package (e.g. publish timeout)
|
|
242
|
-
| Between packages
|
|
243
|
-
| Registry wait timeout
|
|
267
|
+
| Died at | State | Re-run does |
|
|
268
|
+
| ----------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
269
|
+
| Preflight | Nothing mutated anywhere | Everything, after you fix the reported list |
|
|
270
|
+
| Mid-package (e.g. publish timeout) | That package partially released | release-kit's own idempotency finishes it |
|
|
271
|
+
| Between packages | Earlier packages fully released | Skips them (tag exists, version published), continues |
|
|
272
|
+
| Registry wait timeout | Dependency published, dependent untouched | Wait resumes; registry has had more time |
|
|
273
|
+
| Between the version write and its commit (a lockfile refresh or hook failing) | That package's tree is dirty | Preflight refuses the dirty tree; finish that package with release-kit, then re-run |
|
|
274
|
+
|
|
275
|
+
A member whose `release.config.json` sets `requireGreen` is refused in preflight when the
|
|
276
|
+
train commits to its repository before its turn (its own `chore(deps)` commit, or an
|
|
277
|
+
earlier member's release in the same repository): CI cannot have passed that commit yet,
|
|
278
|
+
so release-kit would stop the train there, after earlier members had released.
|
|
244
279
|
|
|
245
280
|
The invariant throughout: at no point does a published package depend on an unpublished
|
|
246
281
|
version, because dependencies always complete first.
|
|
@@ -315,12 +350,14 @@ Flag rules, mirroring release-kit's posture:
|
|
|
315
350
|
- `--dry-run` composes with everything: it is always "show me, touch nothing".
|
|
316
351
|
- `--offline` degrades honestly: the plan says which checks were skipped, and seed-tags
|
|
317
352
|
skips npm members it cannot verify rather than guessing.
|
|
353
|
+
- `--offline` plans only: a release publishes and waits on the registry, so `--offline`
|
|
354
|
+
without `--dry-run` is refused.
|
|
355
|
+
- `--yes` skips the confirmation, as in release-kit; without a terminal it is required.
|
|
318
356
|
- Deliberately absent: a `--bump <type>` override (forcing one bump across packages is
|
|
319
357
|
lockstep by the back door; release one package explicitly instead and let derivation do
|
|
320
|
-
the rest) and a declared-order override (see Considered and declined). `--
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
a machine-readable plan.
|
|
358
|
+
the rest) and a declared-order override (see Considered and declined). A `--json` plan
|
|
359
|
+
output for CI is the one addition under consideration — release-kit writes
|
|
360
|
+
`$GITHUB_OUTPUT`, and the train's equivalent is a machine-readable plan.
|
|
324
361
|
|
|
325
362
|
## Considered and declined
|
|
326
363
|
|
|
@@ -335,10 +372,10 @@ Flag rules, mirroring release-kit's posture:
|
|
|
335
372
|
|
|
336
373
|
## Open questions
|
|
337
374
|
|
|
338
|
-
- **
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
375
|
+
- **Batching a monorepo's releases into one commit** — each member releases with its own
|
|
376
|
+
commit today (see Execute). One commit for all of them is less history noise, but needs
|
|
377
|
+
release-kit to stop before its commit and resume after; worth it only if the per-member
|
|
378
|
+
commits turn out to be a problem.
|
|
342
379
|
- **GitHub releases in a multi-package repo** — `gh release create` per tag works; whether
|
|
343
380
|
the nested-package changelog path (`packages/x/CHANGELOG.md`) needs anything from
|
|
344
381
|
release-kit beyond cwd-relative resolution needs verification.
|