Skip to content

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 with cargo install cargo-release (not a workspace dependency). Config is in release.toml.
  • Cadence & owner: one bump per shipped plan, run by the architect in the close ceremony — after the plan flips to done and its docs land. Not per phase commit.
  • No push: cargo-release stages the version edit and writes the vX.Y.Z tag but does not push; the user pushes (project no-auto-push rule).

Commands

Terminal window
# 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.

Terminal window
git push --follow-tags

Push 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:

Terminal window
git push origin :refs/tags/vX.Y.Z
git push origin vX.Y.Z

That 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 signed
ritmolux-v<version>-windows-x64.zip # ritmolux.exe
ritmolux-v<version>-foobar2000-component.zip # foo_ritmolux.fb2k-component, x64
ritmolux-studio-v<version>-macos-universal.zip # universal Studio.app, ad-hoc signed
ritmolux-studio-v<version>-windows-x64.zip # Ritmolux Studio.exe

Each 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:

Terminal window
gh auth refresh -s workflow

To 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:

Terminal window
.\packaging\foobar\fetch-sdk.ps1 # once; idempotent
.\packaging\foobar\build-component.ps1 # same script, same checks, as CI runs

So can the Windows studio, which needs the same Spout SDK because the player it carries is built with the feature:

Terminal window
.\packaging\spout etch-sdk.ps1 # once; idempotent
.\packaging\studiouild-studio.ps1 # same script, same checks, as CI runs

Its 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 the extern "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-release does not run it — but since ADR-0025 the component version is no longer independent: plugin-foobar/build.ps1 reads [workspace.package].version out of root Cargo.toml and generates build/foo_ritmolux_version.h, which DECLARE_COMPONENT_VERSION consumes. 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 running build.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.Z git tag and all three release-zip names (NFR section 8).
  • The macOS bundle’s CFBundleShortVersionString / CFBundleVersion, substituted into packaging/macos/Info.plist.in at package time. bundle.sh asserts the plist and [workspace.package] agree, so a drift fails the build rather than shipping.
  • The foobar component’s DECLARE_COMPONENT_VERSION, via the generated build/foo_ritmolux_version.h (ADR-0025). build-component.ps1 reads 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.