Cutting a Release
A release publishes seven artifacts to three registries, through six workflows. All of them are triggered automatically when a GitHub Release is published, so the bulk of cutting a release is: get the version numbers right, land the bump, then draft the release.
What gets published
| Artifact | Registry | Workflow | Source | Version comes from |
|---|---|---|---|---|
uniffi-bindgen-react-native | npm | npm.yml | repo root | package.json |
@ubjs/core | npm | npm-core.yml | typescript | typescript/package.json |
@ubjs/node + @ubjs/node-<platform> | npm | napi-publish.yml | runtimes/napi | runtimes/napi/package.json |
@ubjs/wasm | npm | npm-wasm.yml | runtimes/wasm | runtimes/wasm/package.json |
uniffi-runtime-javascript | crates.io | crates-io.yaml | crates/uniffi-runtime-javascript | crates/uniffi-runtime-javascript/Cargo.toml |
uniffi-runtime-wasm | crates.io | crates-io.yaml | runtimes/wasm/helper-crate | runtimes/wasm/helper-crate/Cargo.toml |
uniffi-bindgen-react-native (Pod) | CocoaPods | cocoapods.yml | uniffi-bindgen-react-native.podspec | package.json (the podspec reads package['version']) |
crates-io.yaml publishes both crates from one matrixed job, with
fail-fast: false — neither crate depends on the other, so one failure does
not cancel the other’s publish.
@ubjs/node is the N-API runtime. Its workflow first builds a native binary
for every supported target (macOS x64/arm64, Linux gnu/musl on x64/arm64,
Windows x64/arm64), publishes each as a platform package
(@ubjs/node-darwin-arm64, @ubjs/node-linux-x64-gnu, …), then publishes the
@ubjs/node root package whose optionalDependencies point at them. If the
build matrix fails for any target, the publish job does not run.
@ubjs/wasm is the wasm2 player runtime, and uniffi-runtime-wasm is the
helper crate a consuming cdylib links. @ubjs/wasm declares @ubjs/core as a
peerDependency, so that range has to move with the version too.
Steps
- Increment the version number, keeping all seven files in sync:
package.json(also drives the CocoaPod)crates/ubrn_cli/Cargo.tomlcrates/uniffi-runtime-javascript/Cargo.tomltypescript/package.json(the@ubjs/coreruntime)runtimes/napi/package.json(the@ubjs/noderuntime)runtimes/wasm/package.json(the@ubjs/wasmruntime — also its@ubjs/corepeerDependencyrange)runtimes/wasm/helper-crate/Cargo.toml(theuniffi-runtime-wasmcrate)
- Update the lockfiles to follow, rather than editing them by hand:
cargo metadata --offline > /dev/nullrefreshesCargo.locknpm install --package-lock-onlyin each oftypescript,runtimes/napiandruntimes/wasm
- Update the version references outside the manifests:
docs/src/reference/config-yaml.md— theruntimeVersiondefaultruntimes/wasm/helper-crate/README.md— theCargo.tomlsnippetcrates/ubrn_cli/fixtures/defaults/package.json— the@ubjs/coredependency- Leave statements dating a feature to the release that introduced it (e.g.
“As of
0.31.0-3” in the Node.js reference) alone — those are history.
- Update the CHANGELOG. If the CHANGELOG is up-to-date, then this should be minimal.
- Add a new version title at the top
- Update the Full Changelog link to go from new release to main
- Move the bottom of the “upcoming release” section to the top
- Update the Full Changelog link to go from previous release to new release
- Push as a PR as usual, with subject:
Release ${VERSION_NUMBER}. - (Optional but recommended) Run a dry-run of the publish workflows — see Testing a release before tagging.
- Once the PR has landed, draft a new release.
- Create a new tag (in the choose-a-tag dialog). The tag is the version
exactly as it appears in
package.json, with novand no abbreviation —0.31.0-5, neverv0.31.0-5and never0.31-5. Copy it, do not retype it:node -p "require('./package.json').version" - Use that same version with a
vprepended for the release title:v${VERSION_NUMBER}. Thevbelongs to the title only, never the tag. - Publish the release.
- Wait for the six publish workflows to go green:
- Verify the release landed.
- Tell your friends, make a song and dance, you’ve done a new release.
Testing a release before tagging
Five of the six publish workflows can be run manually from the Actions tab
(workflow_dispatch) with a dry-run input that defaults to true. Use this
to validate packaging — cargo publish --dry-run, npm publish --dry-run —
without pushing anything to a registry:
npm.yml—dry-runinputnpm-core.yml—dry-runinputnpm-wasm.yml—dry-runinputcrates-io.yaml—dry_runinputnapi-publish.yml—dry-runinput
cocoapods.yml
has no dry-run input. Triggering it manually runs a real pod trunk push.
To only validate the podspec, run pod spec lint uniffi-bindgen-react-native.podspec
locally instead.
A real release fires every workflow on the release: published event; the
dry-run path is reachable only through manual workflow_dispatch.
After publishing
Confirm each artifact actually went out:
- npm:
npm view <pkg> versionforuniffi-bindgen-react-native,@ubjs/core,@ubjs/nodeand@ubjs/wasm - crates.io: https://crates.io/crates/uniffi-runtime-javascript/versions and https://crates.io/crates/uniffi-runtime-wasm/versions
- CocoaPods:
pod trunk info uniffi-bindgen-react-native
If a workflow fails part-way, re-run just that workflow from the Actions tab
(workflow_dispatch, dry-run false) once the underlying problem is fixed —
you do not need to cut a new tag. npm and crates.io reject re-publishing a
version that already exists, so a re-run after a partial @ubjs/node publish
will skip the platform packages that already landed and publish the rest.
Version numbers
A release version is always exactly this shape:
MAJOR . MINOR . 0 - N
└────┬────┘ │ └── variant number, monotonic
│ └────── always literally 0
└──────────────── tracks the uniffi-rs release
MAJOR.MINORtracks theuniffi-rsrelease the bindings are built against.uniffi-rs0.31.xgives0.31.- The patch is always
0. We do not mirror theuniffi-rspatch level. Ifuniffi-rsgoes0.31.0→0.31.4, ourMAJOR.MINORis unchanged and onlyNmoves. The0is a placeholder: semver requires three numeric components, so we cannot write0.31-5(see below). Nincreases monotonically across releases and is not reset when theuniffi-rsversion changes. If the last release was0.30.0-1anduniffi-rsis bumped to0.31, the next release is0.31.0-2, not0.31.0-0.
Older releases (0.28.3-5, 0.29.3-1) do carry a non-zero patch, from when the
scheme mirrored the uniffi-rs patch level. They are history; do not copy them.
One string drives everything
The same string is the npm version, the crate version, the CocoaPod version and
the git tag, because the podspec derives its source tag from package.json:
s.version = package['version']
s.source = { :git => ..., :tag => s.version.to_s }
So a tag that does not match package.json exactly fails CocoaPods lint with
Remote branch <version> not found in upstream origin, and every other publish
workflow goes red alongside it.
These are semver prereleases
Anything after the - is a semver prerelease identifier, so 0.31.0-5 reads
as “a prerelease of 0.31.0” and sorts below 0.31.0. Every release we have
ever cut is, to npm and cargo, a prerelease. Two consequences:
npm publishmust pass--tag latest. npm 11 (bundled with node 24) refuses to publish a prerelease without an explicit dist-tag, rather than silently movinglatestonto it. All four npm publish workflows pass it on everynpm publishinvocation, dry-run included; a new one must too.- Consumers need an explicit version.
npm install uniffi-bindgen-react-nativeresolves via thelatestdist-tag, which we set — but a bare semver range like^0.31.0will not match0.31.0-5.
The patch cannot be dropped from the string
Tempting, but neither toolchain accepts it — semver mandates all three
components before a - suffix:
$ npm publish --dry-run # version = "0.31-5"
npm error Invalid version: "0.31-5"
$ cargo metadata # version = "0.31-5"
error: unexpected character '-' after minor version number
“Dropping the patch” is therefore a policy — we stop tracking the uniffi-rs
patch level — not a change to the string. The .0 stays.
Compatibility with other packages
Other versioning we should take care to note:
- React Native
create-react-native-library
Compatibility matrices are built by the nightly matrix (iOS + Android, the full date-derived window) and gated per-PR by the PR gate (latest RN only).
The matrix is date-derived: .github/scripts/compat-matrix.mjs picks, for
each React Native release published in the last 365 days, the
create-react-native-library version and CI runner image that were current at
that RN’s publish date. Runner images come from
.github/compat-runner-schedule.json — the one place to maintain. When
GitHub ships a new macOS/ubuntu generation, append a row
({ "since": "<GA date>", "ios": "<label>", "android": "<label>" }); when an
old label is retired, bump the oldest row to the oldest still-hosted label.