Skip to content

bin: bump versions with a script that owns the plan, commit and tags - #304

Open
gnidan wants to merge 4 commits into
mainfrom
build-release-version
Open

gnidan wants to merge 4 commits into
mainfrom
build-release-version

Conversation

@gnidan

@gnidan gnidan commented Sep 17, 2026

Copy link
Copy Markdown
Member

Prereleases get a named identifier, and bin/version.ts takes over the version bump from Lerna. @ethdebug/format moves to X.Y.Z-draft.N, because a prerelease of the spec is a draft; every other workspace moves to X.Y.Z-preview.N, because those packages work but implement a draft. 0.1.0-draft.0 and 0.1.0-preview.0 sort after the numeric 0.1.0-2 that is published today, and both sort before rc.

Lerna 8 cannot produce this scheme, and the 0.1.0-2 release showed why. A keyword bump on a numeric prerelease yields alpha, because Lerna resolves the identifier as --preid || existing || "alpha". An explicit version moves every workspace whose prerelease number is truthy, which is lockstep for -1 and above and changed-only for -0, so the behavior flips per phase. And one run takes one --preid, so draft and preview cannot start a series together. Each of those is a rule we would have to document around, so the script does the bump itself and Lerna keeps two jobs: running scripts and lerna changed.

yarn tsx bin/version.ts [keyword] [--all] [--dry-run]:

  • What moves. Every workspace whose directory changed since its own tag, with changelogs and test files ignored; @ethdebug/format when schemas/ changed, because the schemas live outside the package directory but ship inside it; and every dependent of a moving workspace, so a spec change moves all ten. The report gives each move a reason: changed, schemas, graduates, dependent or all.
  • Next version. semver with the workspace's identifier. prerelease (the default) counts up; patch graduates every prerelease workspace and patches a stable one; preminor, premajor, minor and major start a series, require --all, and refuse to run while any workspace is a prerelease, except that preminor and premajor may abandon a whole-repository draft series for the next one. minor and major are guarded because on a prerelease they graduate in place or jump the series depending on the patch number.
  • Guards. The branch is main, the tree is clean, the nearest annotated tag is a release tag (Lerna's git describe sees annotated tags only, so a foreign tag or a lightweight release tag would hide changes), no tag for a new version exists, no release tag already points at HEAD while a workspace changed, and every changelog has a section for its workspace's new version with nothing left under Unreleased. A dry run prints the report and the findings and exits 0; a real run stops before writing on any finding.
  • Write. The versions and the internal dependency ranges in the ten package.json files, one Publish commit with hooks disabled, one annotated tag per moving workspace. It never pushes; the atomic push stays the operator's step. On a failure after writing starts it prints the undo commands for the stage it reached.

bin/publish-tagged.ts now chooses the dist-tag. A stable version publishes under latest, unless a higher stable version is known, then under release-<major>.<minor>. A prerelease publishes under latest while the package has no stable version, and under its identifier after that, so the first 0.2.0-draft.0 after a stable 0.1.0 cannot become what a plain install gets. The known versions merge the registry's list with the local tags of the package, because the registry document was seen to lag minutes behind a publish. CI can set one tag per publish, since trusted publishing does not cover npm dist-tag.

The series convention for now: all workspaces start a major.minor series together and graduate together with the spec; between those events each workspace moves only when it or a dependency changed, with its own counter. The runbook records the trigger to revisit it: the first time one package needs a long preview, or its own keyword, while the others release stable.

Verified in a throwaway clone: the switch (prerelease --all from 0.1.0-2), a bugc-only release that moved bugc and its four dependents only, a schemas change that moved all ten, patch graduation, a stable-phase prerelease, preminor --all, a never-released workspace, and each failure path (dirty tree, wrong branch, existing tag, lightweight and foreign tags, a write failure with the right undo advice). lerna changed sees the hand-made tags on the next run. 122 tests cover the pure parts of both scripts.

After this merges, one release with prerelease --all switches the published versions to 0.1.0-draft.0 and 0.1.0-preview.0.

@gnidan gnidan added changelog: skip This PR changes schemas/ or a published package without a changelog entry on purpose and removed changelog: skip This PR changes schemas/ or a published package without a changelog entry on purpose labels Sep 17, 2026
@gnidan
gnidan force-pushed the build-release-version branch from 6138e99 to 1a05a69 Compare September 17, 2026 06:56
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ethdebug.github.io/format/pr-preview/pr-304/

Built to branch gh-pages at 2026-09-18 02:52 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@gnidan
gnidan marked this pull request as draft September 17, 2026 21:47
Lerna 8 cannot produce the release scheme this repository wants. A
keyword bump on a numeric prerelease yields "alpha", because Lerna
resolves the identifier as `--preid || existing || "alpha"`. An
explicit version moves every workspace whose prerelease number is
truthy, which is lockstep for -1 and above and changed-only for -0.
One run takes one --preid, so two identifiers cannot start a series
together.

bin/version.ts therefore does the bump itself. Prereleases carry a
named identifier: X.Y.Z-draft.N for @ethdebug/format, whose version is
the specification version, and X.Y.Z-preview.N for every other
workspace. The script asks `lerna changed` which workspaces changed
since their tags, ignoring changelogs and test files, and forces
@ethdebug/format when schemas/ changed, because the schemas live
outside the package directory but ship inside it. Every dependent of
a moving workspace moves too. Each moving workspace gets its own next
version from semver: `prerelease` counts up, `patch` graduates every
prerelease workspace, and the series-start keywords `preminor`,
`premajor`, `minor` and `major` require --all and refuse to run while
a prerelease exists, except that `preminor` and `premajor` may abandon
a whole-repository draft series for the next one.

Before it writes anything, the script reports each move with its
reason, checks that the branch is main, that the tree is clean, that
the nearest annotated tag is a release tag, that no tag for a new
version exists, that no release tag already points at HEAD while a
workspace changed, and that every changelog has a section for its
workspace's new version and nothing left under Unreleased. A dry run
prints the same report and exits 0. A real run then rewrites the
versions and the internal dependency ranges in the package.json
files, commits them as "Publish" with hooks disabled, and creates
one annotated tag per moving workspace. It never pushes. If it fails
after it started writing, it prints the undo commands for the stage
it reached.
CI can set exactly one dist-tag per publish, because trusted
publishing covers `npm publish` and not `npm dist-tag`. A stable
version publishes under "latest", unless a higher stable version is
known, then under "release-<major>.<minor>". A prerelease publishes
under "latest" while the package has no stable version, and under its
identifier ("draft" or "preview") after that.

The known versions are the registry's list merged with the versions
from the local tags of the package. The registry document was seen to
lag minutes behind a publish; the tags do not lag, and they make a
re-run after a partial failure give the same answer.
The versioning model names the identifiers, what moves in a release,
the series convention and the point at which to revisit it, and the
dist-tag rule. Steps 3 and 4 of the runbook use bin/version.ts for the
preview and for the bump, with a table of the keyword per phase and
the filler wording for a changelog section with no changes. The
tag-move recipe creates annotated tags, because `lerna changed`
ignores lightweight ones. A short section records why Lerna does not
bump versions here.
@gnidan gnidan changed the title bin: bump versions through a script that sees schema changes bin: bump versions with a script that owns the plan, commit and tags Sep 18, 2026
@gnidan
gnidan force-pushed the build-release-version branch from a388027 to e72dc58 Compare September 18, 2026 02:48
@gnidan
gnidan marked this pull request as ready for review September 18, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant