@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 +255 -51
- package/TRAIN.md +5 -1
- package/package.json +4 -4
- package/release.mjs +654 -43
- package/train.mjs +50 -11
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.
|
|
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
|
-
| `--
|
|
198
|
-
| `--
|
|
199
|
-
| `--assistant
|
|
200
|
-
| `--
|
|
201
|
-
| `--
|
|
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.
|
|
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,
|
|
290
|
-
|
|
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
|
|
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
|
-
| `
|
|
682
|
-
| `
|
|
683
|
-
| `
|
|
684
|
-
| `
|
|
685
|
-
| `
|
|
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/
|
|
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.
|
|
813
|
+
run: npx @entro314labs/release-kit@2.9.4 minor --yes
|
|
703
814
|
env:
|
|
704
|
-
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
|
|
717
|
-
|
|
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"]
|
|
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@
|
|
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`
|
|
972
|
-
|
|
973
|
-
with
|
|
974
|
-
|
|
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.
|
|
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.
|
|
53
|
-
"oxlint": "^1.
|
|
52
|
+
"oxfmt": "^0.67.0",
|
|
53
|
+
"oxlint": "^1.82.0",
|
|
54
54
|
"semver": "^7.8.5"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
57
|
+
"node": ">=24"
|
|
58
58
|
}
|
|
59
59
|
}
|