Releasing
How the version number moves and what pushing a tag builds. This is a maintainer’s page — it describes the process a release goes through, not anything a user or a preset author needs.
The scheme is decided in ADR-0005; what follows is the operational summary.
One version, one command, once per plan
- Single source of truth: root
Cargo.toml[workspace.package].version. Both crates inherit it (version.workspace = true); nothing else holds an app-version string. - Bump authority:
cargo-release, a dev tool installed withcargo install cargo-release(not a workspace dependency). Config is inrelease.toml. - Cadence & owner: one bump per shipped plan, run by the architect in the close
ceremony — after the plan flips to
doneand its docs land. Not per phase commit. - No push:
cargo-releasestages the version edit and writes thevX.Y.Ztag but does not push; the user pushes (project no-auto-push rule).
Commands
# Preview — cargo-release is dry-run by default, so this changes nothing:cargo release <patch|minor> --no-push
# Do it (bumps the workspace version, commits, tags vX.Y.Z, no push):cargo release <patch|minor> --no-push --no-publish --no-confirm --execute--execute is what makes it real; without it cargo-release only reports what it
would do. push = false / publish = false are already pinned in release.toml
— the explicit flags on the command line are belt-and-braces.
While in the 0.x band: a feature-plan is a minor bump (0.1.0 -> 0.2.0), a fix-only
plan is a patch bump (0.1.0 -> 0.1.1), and a docs/chore-only plan legitimately gets
no bump (choose the level deliberately — this is not a missed step). Reaching 1.0.0 is
a deliberate future act (freezing the C ABI and standalone behavior), never backed into.
Pushing the tag is what builds the artifacts
cargo-release writes the tag; pushing it is what produces downloadable builds. The user
pushes — the architect never does.
git push --follow-tagsPush one tag, not a backlog of them. GitHub does not start workflows for tags pushed in bulk, so
a git push --tags carrying more than three of them fires nothing at all — no Release run, no
artifacts, and no error to read. That is how v0.113.0 came to exist on origin with zero Release
runs: the 2026-09-10 history rewrite force-pushed 131 tags in one command. The recovery is to delete
the remote ref and push it again on its own, because re-pushing an unchanged ref emits no event
either:
git push origin :refs/tags/vX.Y.Zgit push origin vX.Y.ZThat fires .github/workflows/release.yml
(ADR-0038,
ADR-0115),
which builds the macOS standalone, the Windows standalone, the foobar2000 component and the two
studio zips (ADR-0178) in parallel and, only if
all five are green, publishes a GitHub prerelease carrying five zips:
ritmolux-v<version>-macos-universal.zip # universal .app, ad-hoc signedritmolux-v<version>-windows-x64.zip # ritmolux.exeritmolux-v<version>-foobar2000-component.zip # foo_ritmolux.fb2k-component, x64ritmolux-studio-v<version>-macos-universal.zip # universal Studio.app, ad-hoc signedritmolux-studio-v<version>-windows-x64.zip # Ritmolux Studio.exeEach carries a READ-ME-FIRST.txt; the two standalone zips also carry a reference copy of
presets/*.toml. The two studio zips carry a player of their own, at resources/player/
inside the application, so a tester who takes the studio needs nothing else — that is the whole
reason the studio is a release artifact rather than a checkout-only tool. If any build fails, the
release job is skipped and no release exists — there is no half-published state. Re-running
the same tag’s workflow replaces the assets rather than failing.
The publish step asserts the count is exactly five, so a job that silently stopped producing its artifact fails the release instead of shortening it.
Every zip’s name carries the version from [workspace.package], the studio’s included.
studio/package.json has a version field of its own and cargo-release does not touch it, so
the two packaging scripts override it at build time (-c.extraMetadata.version) rather than read
it. The macOS script then reads the version back out of the built Info.plist and fails if it
disagrees with Cargo.toml — which is what stops the studio from ever shipping a stale version
string.
A tag push is outward-facing. Since ADR-0038 this is the last step of every close ceremony
whose tag gets pushed, so every plan close now publishes a public prerelease whether or not that
build was meant for anyone. --prerelease while in 0.x softens the implication; it does not
remove it. If a close should not publish, do not push the tag.
Editing anything under .github/workflows/ needs the workflow OAuth scope on the git
credential. Without it the push is rejected with a scope error that names neither the file nor
the fix:
gh auth refresh -s workflowTo rehearse the builds, run the workflow from the Actions tab (workflow_dispatch): it produces
all five zips as run artifacts. Note that a workflow_dispatch is only offered once the
workflow file exists on the default branch.
A dispatch never publishes, on any ref — a tag included. The release job’s condition names
the event as well as the ref, if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'), so the rehearsal is safe wherever you launch it and you do not have to pick the
ref carefully to stay out of trouble. Plan 0165 Phase 2 made that true; before it, the condition
read the ref alone, a dispatch on a v* tag satisfied it, and the safest-looking rehearsal was
the one that published — which is not hypothetical here: run 31955362251 published v0.70.0
from a dispatch on that tag.
The consequence: a dispatch is no longer a way to publish a tag whose push produced no run. That recovery is the delete-and-re-push above, and it is now the only one.
The component job can also be rehearsed locally, and unlike the macOS bundle it runs on the box this project is developed on:
.\packaging\foobar\fetch-sdk.ps1 # once; idempotent.\packaging\foobar\build-component.ps1 # same script, same checks, as CI runsSo can the Windows studio, which needs the same Spout SDK because the player it carries is built with the feature:
.\packaging\spoutetch-sdk.ps1 # once; idempotent.\packaging\studiouild-studio.ps1 # same script, same checks, as CI runsIts macOS sibling is packaging/studio/bundle-studio.sh, and like packaging/macos/bundle.sh it
needs a Mac — the ad-hoc signature, the lipo calls and the plutil assertions have no Windows
equivalent. Both take --skip-build / -SkipBuild to reuse the release binaries and the
installed node_modules, for iterating on the zip’s layout without paying for an lto = "fat"
rebuild.
While you are here: read the component’s size
foo_ritmolux.dll carries a soft cap of its own — 12,582,912 B (12 MiB)
(Non-functional requirements §4, ADR-0159) — and it grew +910,848 B between Plan 0097 and Plan 0141
without anyone watching, none of it attributed as it landed. You no longer have to measure it.
The recipe above reads its own output’s length and prints it beside the cap:
foo_ritmolux.dll is 9789952 B (77.8 % of the 12582912 B cap)Past 11,324,620 B — 90 % of the cap — that step emits a warning instead of a check mark. It warns and never dies: a release blocked on a byte count is one where someone edits the constant under time pressure at a tag, which is worse than no gate because it also destroys the record.
What is still yours is the row. If the printed figure has moved more than ~100 KB since the last one, add a dated row to the size series in C ABI contract and say what moved it — that table is the only record of the trend, and a trend is what the cap is actually about. The reminder sits here rather than only in that spec because the trigger it replaces — “re-measure when a dependency is added” — was conditioned on an event that never happened, and the growth arrived anyway.
What this does NOT touch
- The C ABI version (
RLX_ABI_VERSION,core-cabi/src/lib.rs) is a separate axis (ADR-0003). It moves only when theextern "C"surface changes shape — never on an app bump, and an ABI bump never implies an app bump. - Dependency versions (exact
=pins, cargo-deny) are unrelated. - The foobar plugin’s build.
cargo-releasedoes not run it — but since ADR-0025 the component version is no longer independent:plugin-foobar/build.ps1reads[workspace.package].versionout of rootCargo.tomland generatesbuild/foo_ritmolux_version.h, whichDECLARE_COMPONENT_VERSIONconsumes. So a bump here reaches foobar’s Components list on the plugin’s next build, with no second string to edit. (This revises ADR-0005’s original “independent plugin version” note.) Since Plan 0102 that next build is the tag push, not a developer runningbuild.ps1— and the packaging recipe fails the release if the DLL does not carry the workspace version, so the two cannot drift silently. - The pinned foobar2000 SDK (
packaging/foobar/sdk-pin.ps1) is a separate axis and never moves on an app bump. Moving it is its own commit, and it owes the on-device check in On-device validation — nothing in CI can load foobar2000.
Where the version surfaces
- The standalone window title (
env!("CARGO_PKG_VERSION"), resolves to the workspace version). - The
vX.Y.Zgit tag and all three release-zip names (NFR section 8). - The macOS bundle’s
CFBundleShortVersionString/CFBundleVersion, substituted intopackaging/macos/Info.plist.inat package time.bundle.shasserts the plist and[workspace.package]agree, so a drift fails the build rather than shipping. - The foobar component’s
DECLARE_COMPONENT_VERSION, via the generatedbuild/foo_ritmolux_version.h(ADR-0025).build-component.ps1reads the version back out of the linked DLL and asserts it matches[workspace.package], so — as with the macOS plist — a drift fails the build rather than shipping.
All four read the one string in root Cargo.toml — none is edited by hand.
Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.