@entro314labs/release-kit 2.5.0 → 2.7.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 +55 -7
- package/TRAIN.md +332 -0
- package/package.json +5 -2
- package/release.mjs +255 -91
- package/train.mjs +1241 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Released v2.5.0
|
|
|
58
58
|
| [📚 Libraries versus apps](#-libraries-versus-apps) | which steps you want, and why |
|
|
59
59
|
| [🤖 Assistant](#-assistant-optional) | optional AI drafting |
|
|
60
60
|
| [🌍 Any language](#-any-language) | Rust, Python, tag-only, anything |
|
|
61
|
+
| [🚂 Release trains](#-release-trains) | monorepos and multi-repo workspaces |
|
|
61
62
|
| [✅ Preflight](#-preflight) | what is checked before anything mutates |
|
|
62
63
|
| [♻️ Recovering from a failed run](#️-recovering-from-a-failed-run) | why re-running is safe |
|
|
63
64
|
| [⚙️ Configuration](#️-configuration) | `release.config.json`, publishing, auth |
|
|
@@ -177,7 +178,7 @@ Prerelease bumps need `--preid` unless the current version already carries one t
|
|
|
177
178
|
| ---------------------------- | -------------------------------------------------------------------------- |
|
|
178
179
|
| `--only <steps>` | Run only these steps, comma-separated. |
|
|
179
180
|
| `--skip <steps>` | Run every step except these. |
|
|
180
|
-
| `--commit` |
|
|
181
|
+
| `--commit` | Force the `commit` step on when a `steps` config removed it. |
|
|
181
182
|
| `--dry-run` | Print every step, execute nothing. Preflight still runs and still reports. |
|
|
182
183
|
| `--yes`, `-y` | Skip the confirmation prompt. |
|
|
183
184
|
| `--preid <id>` | Prerelease identifier: `alpha`, `beta`, `rc`, `next`, `nightly`, `canary`. |
|
|
@@ -195,7 +196,7 @@ them execute, it never reorders them.
|
|
|
195
196
|
|
|
196
197
|
| Step | Default | What it does |
|
|
197
198
|
| ----------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
198
|
-
| `commit` |
|
|
199
|
+
| `commit` | on\* | Commit a dirty working tree with a drafted message ([assistant](#-assistant-optional) required) |
|
|
199
200
|
| `version` | on | Write the version into `package.json` and `versionFiles` |
|
|
200
201
|
| `changelog` | on | Roll `[Unreleased]` into the version, or add drafted notes |
|
|
201
202
|
| `tag` | on | Annotated git tag carrying the release notes |
|
|
@@ -203,19 +204,25 @@ them execute, it never reorders them.
|
|
|
203
204
|
| `publish` | on | Run the configured `publish` command |
|
|
204
205
|
| `release` | on | Create the GitHub release |
|
|
205
206
|
|
|
207
|
+
\* `commit` is a conditional default: it no-ops on a clean tree, and on a dirty tree it
|
|
208
|
+
proceeds only when a drafting [assistant](#-assistant-optional) is configured — without
|
|
209
|
+
one, preflight still refuses the unclean tree (with a hint), exactly as before. So
|
|
210
|
+
`release-kit auto --assistant auto` releases a dirty tree end to end: stage, drafted
|
|
211
|
+
commit, then the rest of the pipeline. Opt out with `--skip commit` or a `steps` config.
|
|
212
|
+
|
|
206
213
|
`version` and `changelog` write files; those writes are persisted by a release commit made
|
|
207
214
|
automatically when either step runs.
|
|
208
215
|
|
|
209
216
|
```sh
|
|
210
217
|
release-kit minor --skip publish # everything but publish
|
|
211
218
|
release-kit --only tag,push,release # a version already committed elsewhere
|
|
212
|
-
release-kit minor --commit
|
|
219
|
+
release-kit minor --skip commit # never touch uncommitted work
|
|
213
220
|
```
|
|
214
221
|
|
|
215
222
|
Or fix it per project, and just run `release-kit minor`:
|
|
216
223
|
|
|
217
224
|
```json
|
|
218
|
-
{ "steps": ["
|
|
225
|
+
{ "steps": ["version", "changelog", "tag", "push", "release"] }
|
|
219
226
|
```
|
|
220
227
|
|
|
221
228
|
`steps` decides **what** runs. Every other key describes **how** a step behaves — `publish`
|
|
@@ -240,6 +247,14 @@ Notes resolve in this order:
|
|
|
240
247
|
than something that requires an assistant.
|
|
241
248
|
4. Otherwise GitHub generates them from the commits since the previous tag.
|
|
242
249
|
|
|
250
|
+
`--notes <source>` forces one instead of walking that list: `changelog`, `assistant`,
|
|
251
|
+
`commits`, or `github`. A named source that produces nothing is an error rather than a
|
|
252
|
+
quiet fall-through — asking for one thing and being given another is worse than being told
|
|
253
|
+
it is unavailable.
|
|
254
|
+
|
|
255
|
+
`--assistant` names the _tool_; `--notes` names the _source_. Making an assistant available
|
|
256
|
+
does not make it preferred, because a hand-written changelog entry should still win.
|
|
257
|
+
|
|
243
258
|
The same text becomes the tag annotation, the GitHub release body, and (when rolled) the
|
|
244
259
|
changelog entry. It is written once and lands in three places.
|
|
245
260
|
|
|
@@ -383,6 +398,23 @@ Stopping at `push` because the tag is what triggers the build pipeline — see
|
|
|
383
398
|
The project name comes from the manifest when there is one (`name` in `package.json`,
|
|
384
399
|
`Cargo.toml` or `pyproject.toml`), and falls back to the repository directory.
|
|
385
400
|
|
|
401
|
+
## 🚂 Release trains
|
|
402
|
+
|
|
403
|
+
Interdependent packages — a monorepo, or a plain folder of sibling git repositories —
|
|
404
|
+
release with the second bin in this package:
|
|
405
|
+
|
|
406
|
+
```sh
|
|
407
|
+
release-train --dry-run # plan + whole-train preflight, execute nothing
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`train.mjs` derives the dependency graph and publish order from the package manifests
|
|
411
|
+
(never from declared config), releases dependencies before dependents with release-kit as
|
|
412
|
+
the per-package worker, rewrites internal ranges, and refuses the whole train before
|
|
413
|
+
anything mutates if any package would fail. A `train.config.json` declares only which
|
|
414
|
+
directories are members. Design, configuration and the full pipeline are in
|
|
415
|
+
[TRAIN.md](TRAIN.md). Prototype status: planning, preflight and `seed-tags` work;
|
|
416
|
+
execution is not wired up yet.
|
|
417
|
+
|
|
386
418
|
## ⚙️ Configuration
|
|
387
419
|
|
|
388
420
|
`release.config.json`, beside `package.json`. Every key is optional; unknown keys abort
|
|
@@ -430,6 +462,18 @@ re-deriving it: `version`, `tag`, `name`, `dist-tag`, `steps`, `published`, `rel
|
|
|
430
462
|
Nothing is written on a dry run, and an unwritable `$GITHUB_OUTPUT` never fails a release
|
|
431
463
|
that already completed.
|
|
432
464
|
|
|
465
|
+
Two upstream habits make commit-derived notes trustworthy, and neither is release-kit's job:
|
|
466
|
+
|
|
467
|
+
- **Gate the release on CI, and guard against forks.** Trigger on `workflow_run` after your
|
|
468
|
+
check workflow succeeds, with `if: github.repository_owner == 'your-org'` so a fork never
|
|
469
|
+
tries to release.
|
|
470
|
+
- **Validate pull request titles.** A squash-merge takes its subject from the PR title, so
|
|
471
|
+
that title becomes the commit the notes are built from.
|
|
472
|
+
[`amannn/action-semantic-pull-request`](https://github.com/amannn/action-semantic-pull-request)
|
|
473
|
+
enforces it. Without something like it, work silently goes missing from release notes —
|
|
474
|
+
release-kit says how many commits are not Conventional Commits, but it cannot fix them
|
|
475
|
+
after the fact.
|
|
476
|
+
|
|
433
477
|
Three things CI does that are worth knowing about:
|
|
434
478
|
|
|
435
479
|
- **`fetch-depth: 0`.** The default checkout is a shallow clone, which hides the history
|
|
@@ -546,13 +590,17 @@ downgrade, so a configured pipeline fails loudly; `"auto"` degrades quietly by d
|
|
|
546
590
|
|
|
547
591
|
### What it does
|
|
548
592
|
|
|
549
|
-
-
|
|
550
|
-
staged
|
|
593
|
+
- **A dirty working tree is committed instead of refusing to release.** The tree is
|
|
594
|
+
staged, a Conventional Commits message is drafted for the staged diff, and the commit is
|
|
595
|
+
made — by default, whenever an assistant is configured (`--skip commit` opts out). The subject is validated
|
|
551
596
|
against the Conventional Commits grammar; an answer that does not parse is rejected rather
|
|
552
597
|
than committed. Attribution lines (`Co-Authored-By`, `Generated with`) are stripped, so
|
|
553
598
|
the tool never signs your commits.
|
|
554
599
|
- **Release notes** are drafted from the commits since the last tag when `CHANGELOG.md` has
|
|
555
|
-
no section for the version.
|
|
600
|
+
no section for the version. Each bullet ends with a link to the commits it covers: the
|
|
601
|
+
assistant is given the short hashes and asked to cite them, and every citation is checked
|
|
602
|
+
against the commits that actually exist. Models invent plausible-looking hashes, so an
|
|
603
|
+
unrecognised one is removed rather than published as a link to nothing. They are written into the changelog, used as the tag
|
|
556
604
|
annotation, and posted as the GitHub release body — the same "written once, lands in three
|
|
557
605
|
places" path a hand-written section takes.
|
|
558
606
|
|
package/TRAIN.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# release-train
|
|
2
|
+
|
|
3
|
+
Orchestrated releases for a set of interdependent packages, in dependency order, using
|
|
4
|
+
release-kit as the per-package worker. One command releases a "train": every package that
|
|
5
|
+
changed, plus every package that depends on one that did, each with its own version bump,
|
|
6
|
+
changelog, tag, push, publish and GitHub release.
|
|
7
|
+
|
|
8
|
+
Ships in this package as `train.mjs` — a second self-contained, `node:*`-only file beside
|
|
9
|
+
`release.mjs`, installed as the `release-train` bin. Same design contract as release-kit:
|
|
10
|
+
readable, vendorable, zero dependencies.
|
|
11
|
+
|
|
12
|
+
**Status: prototype.** Discovery, graph derivation, registry-aware change detection,
|
|
13
|
+
cascade, planning, whole-train preflight, `seed-tags`, and the train summary work.
|
|
14
|
+
Execution (running release-kit per package) is not implemented yet — `train` without
|
|
15
|
+
`--dry-run` says so and exits.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
release-train graph # print the derived dependency graph and topo order
|
|
19
|
+
release-train --dry-run # full plan + whole-train preflight, execute nothing
|
|
20
|
+
release-train --dry-run --all # plan every member, not just changed ones
|
|
21
|
+
release-train --dry-run <id>... # plan these packages and their dependents
|
|
22
|
+
release-train seed-tags # baseline tags at each HEAD (--dry-run to preview)
|
|
23
|
+
release-train --summary <path> # write the train summary; --assistant drafts on top
|
|
24
|
+
release-train --offline # no network: registry checks skipped, tags not pushed
|
|
25
|
+
release-train --config <path> # config elsewhere than ./train.config.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Problem
|
|
29
|
+
|
|
30
|
+
A single package release is solved (release-kit). What is not solved is the shape where
|
|
31
|
+
package B depends on package A, so releasing A implies:
|
|
32
|
+
|
|
33
|
+
1. publish A first,
|
|
34
|
+
2. move B onto A's new version,
|
|
35
|
+
3. release B — and repeat transitively for anything that depends on B.
|
|
36
|
+
|
|
37
|
+
Doing this by hand means remembering the order, editing dependency ranges, and hoping the
|
|
38
|
+
registry has A before B publishes. Existing tools (changesets, Lerna, Nx Release) solve it
|
|
39
|
+
only for a single git repository. Nothing mainstream solves it for a workspace of sibling
|
|
40
|
+
git repositories, which is the second topology this design covers.
|
|
41
|
+
|
|
42
|
+
## Relationship to release-kit
|
|
43
|
+
|
|
44
|
+
release-train is an **orchestrator, not a release tool**. It never bumps a version, writes
|
|
45
|
+
a changelog, tags, pushes or publishes itself — it decides _which_ packages release, _in
|
|
46
|
+
what order_, rewrites internal dependency ranges, and then runs release-kit once per
|
|
47
|
+
package with the right working directory. Everything release-kit already guarantees
|
|
48
|
+
(accumulate-then-abort preflight, idempotent steps, notes resolution, per-ecosystem
|
|
49
|
+
publish) is inherited per package, unchanged.
|
|
50
|
+
|
|
51
|
+
This split is deliberate:
|
|
52
|
+
|
|
53
|
+
- release-kit keeps its hard refusal to release a nested package when invoked directly —
|
|
54
|
+
the refusal exists because it once silently released the parent. The orchestrator is the
|
|
55
|
+
one caller that knows which nested directory it means, and says so explicitly.
|
|
56
|
+
- Each package keeps its own `release.config.json`. The orchestrator does not know what
|
|
57
|
+
npm, cargo or uv are; the package's own config does.
|
|
58
|
+
- The orchestrator stays small enough to hold to the same standard as release-kit: one
|
|
59
|
+
readable file, `node:*` imports only.
|
|
60
|
+
|
|
61
|
+
## Principles
|
|
62
|
+
|
|
63
|
+
1. **Derive, never declare.** Publish order is a projection of the dependency graph, and
|
|
64
|
+
the graph already exists in the package manifests. A declared order duplicates that
|
|
65
|
+
truth and drifts; a derived order cannot. The same goes for versions: the manifest is
|
|
66
|
+
the truth, no config file stores a copy of it. (Validated empirically: the entrolytics
|
|
67
|
+
ecosystem's `versions.json` stored versions and a declared-dependencies field — every
|
|
68
|
+
sampled version had drifted from its manifest, and the dependencies field was never
|
|
69
|
+
populated at all.)
|
|
70
|
+
2. **Config declares membership, nothing else that changes per release.** Which
|
|
71
|
+
directories are part of the train is a fact discovery cannot always get right (stale
|
|
72
|
+
folders, docs repos, private apps). Order, versions and the graph are always derived.
|
|
73
|
+
3. **Whole-train preflight before anything mutates.** There is no cross-package
|
|
74
|
+
transaction on npm or across git repositories. The compensation is release-kit's
|
|
75
|
+
accumulate-then-abort preflight, widened to every package in the plan: all failures
|
|
76
|
+
from all packages are reported in one list, and nothing is touched until the list is
|
|
77
|
+
empty.
|
|
78
|
+
4. **Idempotency is the resume mechanism.** Because dependencies publish before
|
|
79
|
+
dependents, an interrupted train never leaves a published package depending on an
|
|
80
|
+
unpublished version. Re-running the same command skips completed packages (release-kit
|
|
81
|
+
already skips written versions, existing tags, published versions, existing releases)
|
|
82
|
+
and continues from the first incomplete one. No state file, no `--resume`.
|
|
83
|
+
5. **Independent versions.** Each package bumps by its own history. Lockstep ("global
|
|
84
|
+
version") is not offered: in practice it decays into drift the moment one package needs
|
|
85
|
+
a patch the others do not (observed in the wild), and it forces empty releases.
|
|
86
|
+
|
|
87
|
+
## Topologies
|
|
88
|
+
|
|
89
|
+
Two topologies, one model.
|
|
90
|
+
|
|
91
|
+
**Single-repo workspace (monorepo).** One git repository, packages under globs
|
|
92
|
+
(`packages/*`, `src/app/*`), usually a pnpm/npm/bun workspace. One release commit for the
|
|
93
|
+
whole train, one tag per released package.
|
|
94
|
+
|
|
95
|
+
**Meta-workspace (multi-repo).** A plain folder — not itself a git repository — containing
|
|
96
|
+
sibling git repositories, each with its own remote, branch, history and (possibly) its own
|
|
97
|
+
nested packages. Each package releases as a full release-kit run in its owning repo: its
|
|
98
|
+
own commit, tag, push, publish, GitHub release.
|
|
99
|
+
|
|
100
|
+
**The unit model that covers both:** a _package_ (the publish unit, a directory with a
|
|
101
|
+
manifest) belongs to an _owning repo_ (the git unit, found by walking up to the nearest
|
|
102
|
+
`.git`). A repo may own many packages; in a monorepo, one repo owns all of them; in a
|
|
103
|
+
meta-workspace most repos own exactly one, but a nested monorepo inside a meta-workspace
|
|
104
|
+
is just a repo that owns several. Nothing in the pipeline branches on topology — only on
|
|
105
|
+
"which repo owns this package", which decides where the commit and tag land.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
`train.config.json` at the workspace root. Membership and policy only — no versions, no
|
|
110
|
+
dependencies, no order.
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"packages": [
|
|
115
|
+
"packages/*",
|
|
116
|
+
{ "path": "sdks/react", "id": "react-sdk" },
|
|
117
|
+
{ "path": "apps/dashboard", "publish": false }
|
|
118
|
+
],
|
|
119
|
+
"rangePolicy": "caret",
|
|
120
|
+
"registryWait": { "timeout": 300, "interval": 5 }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Key | Default | Meaning |
|
|
125
|
+
| -------------- | --------- | --------------------------------------------------------------------------------------------- |
|
|
126
|
+
| `packages` | required | Paths or globs. An entry is a string or `{ path, id?, publish? }`. |
|
|
127
|
+
| `rangePolicy` | `"caret"` | How rewritten internal ranges are written: `caret`, `tilde`, `exact`, or `preserve`. |
|
|
128
|
+
| `registryWait` | as shown | How long to poll the registry for a just-published dependency before releasing its dependent. |
|
|
129
|
+
|
|
130
|
+
- `id` names the package in output and on the command line; defaults to the manifest name.
|
|
131
|
+
- `publish: false` keeps a package in the graph (its changes still cascade to dependents,
|
|
132
|
+
its version still bumps) but skips registry publishing — for apps.
|
|
133
|
+
- In a workspace with `pnpm-workspace.yaml` / `workspaces` globs, `packages` may be
|
|
134
|
+
omitted and is taken from there.
|
|
135
|
+
- Everything per-package — publish command, changelog path, version files, assistant —
|
|
136
|
+
lives in that package's own `release.config.json`, exactly as standalone release-kit
|
|
137
|
+
reads it. The orchestrator adds nothing to it and overrides nothing in it.
|
|
138
|
+
- Unknown keys abort, as in release-kit.
|
|
139
|
+
|
|
140
|
+
## Pipeline
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
discover → graph → detect changes → cascade → plan → preflight (whole train) → execute → report
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Discover.** Resolve `packages` globs to directories, read each manifest (name, version,
|
|
147
|
+
dependencies), find each package's owning repo. A membership entry whose path does not
|
|
148
|
+
exist is a preflight failure, not a silent skip.
|
|
149
|
+
|
|
150
|
+
**Graph.** An internal dependency is a manifest dependency (`dependencies`,
|
|
151
|
+
`peerDependencies`, `optionalDependencies`) whose name matches another member package.
|
|
152
|
+
`devDependencies` do not create publish-order edges — they never appear in the published
|
|
153
|
+
artifact — but a devDependency change still marks the dependent as changed. Cycles through
|
|
154
|
+
publish-order edges abort with the cycle printed; there is no order that releases a cycle.
|
|
155
|
+
|
|
156
|
+
**Detect changes.** Two sources, and the registry outranks the commits.
|
|
157
|
+
|
|
158
|
+
_The registry._ One lookup per npm package (`npm view <name> versions`, cached) classifies
|
|
159
|
+
the manifest version:
|
|
160
|
+
|
|
161
|
+
| State | Meaning | Consequence |
|
|
162
|
+
| --------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `current` | manifest version is the registry's latest | normal — bump from commits |
|
|
164
|
+
| `pending` | manifest version is not published (a committed-but-unreleased bump, or a package never published at all) | released **as-is**: the pending version is the release, it joins the set even with zero new commits, and dependents' rewrites target it. Nothing is skipped over. |
|
|
165
|
+
| `behind` | the registry has a newer version than the manifest | preflight **failure** — the tree is behind what shipped; sync it, never guess |
|
|
166
|
+
| `unknown` | lookup failed, or `--offline` | warning — pending/collision checks not performed |
|
|
167
|
+
|
|
168
|
+
`pending` is common in the wild (both reference workspaces had them: versions bumped and
|
|
169
|
+
committed, publish never ran or failed) and is precisely the case a "bump then publish"
|
|
170
|
+
tool gets wrong by bumping past the version that never shipped.
|
|
171
|
+
|
|
172
|
+
_The commits._ Per package: full commit messages (subjects **and** bodies, so
|
|
173
|
+
`BREAKING CHANGE:` footers count) in the owning repo touching the package's path since the
|
|
174
|
+
package's last release tag. Tag scheme is `<name>@<version>` per package (the
|
|
175
|
+
changesets/release-please convention) when a repo owns more than one package, and plain
|
|
176
|
+
`v<version>` when it owns exactly one — which keeps single-package repos identical to
|
|
177
|
+
standalone release-kit. A package with no release tag yet is _cold_ (see Cold start).
|
|
178
|
+
|
|
179
|
+
**Cascade.** Any member that depends on a package in the release set joins the set with at
|
|
180
|
+
least a patch bump, transitively. This is what makes the new dependency version actually
|
|
181
|
+
reach consumers. Each package's own bump is derived by release-kit's `auto` from its
|
|
182
|
+
path-scoped commits; the cascade only raises "no release" to "patch", it never lowers.
|
|
183
|
+
|
|
184
|
+
**Plan.** Topological sort of the release set. The plan is printed before anything runs:
|
|
185
|
+
each package, its current → next version, its bump reason (commits, cascade, or explicit),
|
|
186
|
+
which internal ranges will be rewritten, and the order. `--dry-run` stops here — after
|
|
187
|
+
preflight, so the whole plan and every blocker are visible together, matching release-kit.
|
|
188
|
+
|
|
189
|
+
**Preflight (whole train).** For every package in the plan, before anything mutates
|
|
190
|
+
anywhere:
|
|
191
|
+
|
|
192
|
+
| Check | Scope |
|
|
193
|
+
| -------------------------------------------------------------------------------------- | ----------- |
|
|
194
|
+
| Working tree clean | per repo |
|
|
195
|
+
| On the configured branch, not detached | per repo |
|
|
196
|
+
| Remote reachable, branch not behind it | per repo |
|
|
197
|
+
| Release tag free (or already at `HEAD`, reusable) | per package |
|
|
198
|
+
| Planned version not already on the registry | per package |
|
|
199
|
+
| Manifest not _behind_ the registry (a newer version was published than the tree knows) | per package |
|
|
200
|
+
| Publish CLI authenticated | per package |
|
|
201
|
+
| `gh` authenticated | once |
|
|
202
|
+
| Every internal range will, after rewriting, match the version being published | per edge |
|
|
203
|
+
| A `workspace:` range's target is a member of the train | per edge |
|
|
204
|
+
| Membership paths exist and carry a manifest | per entry |
|
|
205
|
+
| No publish-order cycles | once |
|
|
206
|
+
|
|
207
|
+
One failure anywhere aborts the entire train before any package releases. This is the
|
|
208
|
+
whole safety story for the meta-workspace, where no transaction exists — so it is
|
|
209
|
+
deliberately strict: one dirty repo out of thirty blocks all thirty.
|
|
210
|
+
|
|
211
|
+
**Execute.** In topo order, per package:
|
|
212
|
+
|
|
213
|
+
1. Rewrite internal dependency ranges in this package's manifest to the versions its
|
|
214
|
+
dependencies just released, per `rangePolicy`. Skipped entirely for `workspace:`
|
|
215
|
+
ranges — the package manager rewrites those at publish time, which is the preferred
|
|
216
|
+
setup inside a workspace.
|
|
217
|
+
2. Run release-kit in the package directory: bump, changelog, release commit (the range
|
|
218
|
+
rewrite rides in it), tag, push, publish, GitHub release — whatever that package's
|
|
219
|
+
`steps` say.
|
|
220
|
+
3. If any member still to come depends on this package: poll the registry
|
|
221
|
+
(`npm view name@version` or the ecosystem equivalent) until the new version is
|
|
222
|
+
visible or `registryWait.timeout` elapses. Registries have replication lag; a
|
|
223
|
+
dependent that publishes or installs too early fails spuriously.
|
|
224
|
+
|
|
225
|
+
In a monorepo, step 2's commits per package would produce commit noise; there the
|
|
226
|
+
orchestrator batches: all version/changelog/range writes land in one release commit, then
|
|
227
|
+
tags, one push, then publishes in topo order with the same waits.
|
|
228
|
+
|
|
229
|
+
**Report.** What released at which version, what was skipped and why, and — on failure —
|
|
230
|
+
exactly which packages completed, so the resume story ("run it again") is verifiable.
|
|
231
|
+
|
|
232
|
+
## Failure and resume
|
|
233
|
+
|
|
234
|
+
| Died at | State | Re-run does |
|
|
235
|
+
| ---------------------------------- | ----------------------------------------- | ----------------------------------------------------- |
|
|
236
|
+
| Preflight | Nothing mutated anywhere | Everything, after you fix the reported list |
|
|
237
|
+
| Mid-package (e.g. publish timeout) | That package partially released | release-kit's own idempotency finishes it |
|
|
238
|
+
| Between packages | Earlier packages fully released | Skips them (tag exists, version published), continues |
|
|
239
|
+
| Registry wait timeout | Dependency published, dependent untouched | Wait resumes; registry has had more time |
|
|
240
|
+
|
|
241
|
+
The invariant throughout: at no point does a published package depend on an unpublished
|
|
242
|
+
version, because dependencies always complete first.
|
|
243
|
+
|
|
244
|
+
## Cold start
|
|
245
|
+
|
|
246
|
+
A package with no release tag cannot compute "commits since last tag". Options, chosen per
|
|
247
|
+
run, not configured:
|
|
248
|
+
|
|
249
|
+
- `train seed-tags` — create `<name>@<manifest version>` (or `v<version>`) at each repo's
|
|
250
|
+
current `HEAD`, push tags, release nothing. The next train has a baseline. This is the
|
|
251
|
+
right move for an existing ecosystem whose versions are already published.
|
|
252
|
+
- `--all` — treat every member as changed and release everything once.
|
|
253
|
+
|
|
254
|
+
Seeding refuses, per member and without touching it, when:
|
|
255
|
+
|
|
256
|
+
- the manifest version is not on the registry — tagging a commit as "released 2.4.2" when
|
|
257
|
+
2.4.2 never shipped would make the baseline a lie. A pending version is _released_ by
|
|
258
|
+
the train (as-is), then it has a real tag;
|
|
259
|
+
- the manifest is behind the registry — sync the tree first;
|
|
260
|
+
- the manifest file has uncommitted changes — the version on disk may not be the version
|
|
261
|
+
at `HEAD`, so the tag would point at the wrong commit;
|
|
262
|
+
- there is no manifest version (go, tag-only projects) or the registry cannot be reached —
|
|
263
|
+
nothing can vouch for the baseline; seed those by hand.
|
|
264
|
+
|
|
265
|
+
`seed-tags --dry-run` previews every action; `--offline` creates tags without pushing.
|
|
266
|
+
|
|
267
|
+
## Cross-ecosystem notes
|
|
268
|
+
|
|
269
|
+
The orchestrator's graph is built from npm-style manifests today. Non-npm members still
|
|
270
|
+
participate:
|
|
271
|
+
|
|
272
|
+
- **Go**: no manifest version; the tag is the release. release-kit already handles it
|
|
273
|
+
(`versionFile: null`). It has no npm-visible dependents, so no registry wait.
|
|
274
|
+
- **Python / PHP**: `pyproject.toml` / `composer.json` versions, publish via the package's
|
|
275
|
+
own configured command; Packagist releases _are_ the pushed tag.
|
|
276
|
+
- Cross-ecosystem dependency edges (a Python package tracking an npm package's version)
|
|
277
|
+
are out of scope for ordering; they version independently.
|
|
278
|
+
|
|
279
|
+
## CLI
|
|
280
|
+
|
|
281
|
+
Two commands and four flags; modes are commands, modifiers are flags.
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
train # plan + preflight + release everything that changed (or is pending)
|
|
285
|
+
train --dry-run # plan + preflight, execute nothing
|
|
286
|
+
train react-sdk # release this package and its dependents only
|
|
287
|
+
train --all # every member, cold start or forced full train
|
|
288
|
+
train seed-tags # establish baseline tags, release nothing (--dry-run to preview)
|
|
289
|
+
train graph # print the derived graph and topo order
|
|
290
|
+
train --offline # no network: registry checks skipped, tags not pushed
|
|
291
|
+
train --config <path> # config file elsewhere than ./train.config.json
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Flag rules, mirroring release-kit's posture:
|
|
295
|
+
|
|
296
|
+
- Unknown flags abort — a typo must never silently change behaviour.
|
|
297
|
+
- `--all` and explicit package ids conflict and abort; `graph` takes neither.
|
|
298
|
+
- `--dry-run` composes with everything: it is always "show me, touch nothing".
|
|
299
|
+
- `--offline` degrades honestly: the plan says which checks were skipped, and seed-tags
|
|
300
|
+
skips npm members it cannot verify rather than guessing.
|
|
301
|
+
- Deliberately absent: a `--bump <type>` override (forcing one bump across packages is
|
|
302
|
+
lockstep by the back door; release one package explicitly instead and let derivation do
|
|
303
|
+
the rest) and a declared-order override (see Considered and declined). `--yes` arrives
|
|
304
|
+
with execution, matching release-kit. A `--json` plan output for CI is the one addition
|
|
305
|
+
under consideration — release-kit writes `$GITHUB_OUTPUT`, and the train's equivalent is
|
|
306
|
+
a machine-readable plan.
|
|
307
|
+
|
|
308
|
+
## Considered and declined
|
|
309
|
+
|
|
310
|
+
| Idea | Why not |
|
|
311
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
312
|
+
| Declared release order in config | Duplicates the graph, drifts, cannot express independence or verify itself. The graph is already in the manifests. |
|
|
313
|
+
| Versions stored in a central file | A copy of the manifest that goes stale. Observed failing in a real ecosystem within weeks. The manifest is the version. |
|
|
314
|
+
| Lockstep / global version | Decays into drift the first time one package needs a solo patch; forces empty releases of unchanged packages. |
|
|
315
|
+
| Changeset intent files | A second workflow to learn and enforce. Conventional Commits already carry the intent, and release-kit's `auto` already reads them per path. |
|
|
316
|
+
| Parallel publishing of independent packages | Real wall-clock win, real interleaved-output and rate-limit cost. Sequential is comprehensible; revisit only if train duration becomes a problem. |
|
|
317
|
+
| Baking orchestration into release.mjs | Would grow the single file past readability and reopen the nested-package refusal for every standalone user. Two small files beat one large one. |
|
|
318
|
+
|
|
319
|
+
## Open questions
|
|
320
|
+
|
|
321
|
+
- **Monorepo commit batching** — the batched single-commit path shares release-kit's steps
|
|
322
|
+
but reorders when the commit happens; whether that is a release-kit flag
|
|
323
|
+
(`--no-commit`, commit handled by caller) or orchestrator-side sequencing needs a
|
|
324
|
+
decision before implementation.
|
|
325
|
+
- **GitHub releases in a multi-package repo** — `gh release create` per tag works; whether
|
|
326
|
+
the nested-package changelog path (`packages/x/CHANGELOG.md`) needs anything from
|
|
327
|
+
release-kit beyond cwd-relative resolution needs verification.
|
|
328
|
+
- **Partial trains and humans** — `train react-sdk` releases a package and its dependents,
|
|
329
|
+
but _not_ its dependencies. If a dependency has unreleased changes, is that a warning or
|
|
330
|
+
a refusal?
|
|
331
|
+
- **Private registries / scoped auth per package** — preflight currently assumes one
|
|
332
|
+
registry identity per publish CLI; per-package `.npmrc` scoping needs a pass.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entro314labs/release-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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",
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
"url": "git+https://github.com/entro314-labs/release-kit.git"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
|
-
"release-kit": "release.mjs"
|
|
28
|
+
"release-kit": "release.mjs",
|
|
29
|
+
"release-train": "train.mjs"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"release.mjs",
|
|
33
|
+
"train.mjs",
|
|
32
34
|
"README.md",
|
|
35
|
+
"TRAIN.md",
|
|
33
36
|
"LICENSE"
|
|
34
37
|
],
|
|
35
38
|
"type": "module",
|