Releases are automated with release-plz and driven by
Conventional Commits. In normal
operation you never run cargo publish by hand — you merge a PR.
- Land feature/fix PRs to
mainwith conventional-commit messages (feat(runtime): …,fix(parser): …, etc.). - The
release-plz-prjob opens (and keeps updating) a single Release PR titled likechore(release): 0.2.0. It bumps the workspace version and regenerates the rootCHANGELOG.mdfrom the commits since the last release. - Review that PR. When it looks right, merge it.
- Merging is a push to
main, which triggers therelease-plz-releasejob: it publishes the crates to crates.io, then creates onev0.x.ytag and one GitHub Release.
That's it. To cut a release, you merge the Release PR.
To see what the next release would look like before any of this, run
mise run release-preview. It rewrites Cargo.toml and CHANGELOG.md in place
without touching the remote — inspect with git diff, then git checkout ..
The Release PR does not run CI. It's opened with the default
GITHUB_TOKEN, and GitHub does not start new workflow runs for events raised by that token. This is expected: the Release PR only bumps versions and changelogs, and the feature PRs that fed it already passed CI. (To get CI on the Release PR, swap the token for a PAT or GitHub App — see Auth below.)
The crates share one version (version.workspace = true in the root
Cargo.toml), so the whole workspace is released in lockstep under a single
version number.
While the project is pre-1.0, release-plz applies Cargo's 0.x semver rules from the commit types:
| Commits since last release | Bump | Example |
|---|---|---|
fix: / feat: |
patch | 0.1.0 → 0.1.1 |
feat!: or a BREAKING CHANGE: footer |
minor | 0.1.0 → 0.2.0 |
Publishable crates (released together):
vihaco,vihaco-abi,vihaco-abi-derive,vihaco-bytecode,vihaco-cpu,vihaco-module,vihaco-parser,vihaco-parser-derive,vihaco-runtime,vihaco-runtime-derive,vihaco-stdlib,vihaco-syntax
vihaco-doctests is publish = false, so release-plz skips it automatically.
version.workspace = true is not by itself enough to make release-plz treat the
workspace as one unit: it infers a bump per crate and would rewrite that one
shared field on behalf of whichever crate happened to change. So every published
crate is pinned into version_group = "workspace" in release-plz.toml. Each
member then gets the highest bump any member warrants, and they all release
together.
That is also what keeps the single tag working — see below.
release-plz's default naming is per-package, and 0.3.0 shipped that way: twelve
tags and twelve GitHub Releases for one workspace version. Because GitHub's
"Latest" badge is per-repository, it landed arbitrarily on
vihaco-abi-derive-v0.3.0. release-plz.toml now collapses that into a single
v0.x.y:
| Key | Where | Why |
|---|---|---|
git_tag_name = "v{{ version }}" |
[workspace] |
Default is {{ package }}-v{{ version }}. |
git_release_name = "v{{ version }}" |
[workspace] |
A separate key — it does not inherit from git_tag_name. Left at its default, the tag would read v0.3.1 but the Release title would still read vihaco-v0.3.1. |
git_tag_enable = false, git_release_enable = false |
[workspace] |
With the name templated above, twelve packages would each try to create the identical tag and collide. Creation is re-enabled on vihaco only. |
changelog_update = false |
[workspace] |
Re-enabled on vihaco only, so exactly one changelog is written. |
changelog_path = "CHANGELOG.md" |
[[package]] vihaco |
One changelog at the repo root. Relative to the root Cargo.toml, and cannot be set in [workspace] — it is a package-only field. |
changelog_include = [...] |
[[package]] vihaco |
Makes the release body complete. See below. |
vihaco is the tag owner. It is also 11th of 12 in publish order, so the tag
lands after nearly every crates.io publish has already succeeded.
release-plz attributes a commit to a package by which package directory the
changed files live in. So vihaco's changelog would otherwise contain only
commits touching crates/vihaco/ — a fix(parser): confined to
crates/vihaco-parser/ would be missing from the release notes entirely, with
nothing failing. changelog_include lists the other eleven published crates so
their commits land in the one changelog too.
Commits that touch no package directory (docs/, README.md, the root
Cargo.toml, CI) belong to no package and appear in no changelog. That is
release-plz's model, not a misconfiguration.
| Target | Mechanism | Notes |
|---|---|---|
| GitHub | default GITHUB_TOKEN |
No PAT/App, so no org-admin setup. Tradeoff: no CI on the Release PR (above). |
| crates.io | OIDC trusted publishing | No stored token. id-token: write on the release job lets release-plz mint a short-lived token. |
crates.io trusted publishing is configured per crate (you must be a crate
owner — no GitHub org access needed): for each crate, go to
https://crates.io/crates/<name>/settings → Trusted Publishing → GitHub
and set owner QuEraComputing, repo vihaco, workflow release-plz.yml,
environment (blank).
Each job runs with least-privilege permissions: the PR job uses
contents: write + pull-requests: write; the release job uses
contents: write + pull-requests: read + id-token: write. The
pull-requests: read is required — release-plz lists the PRs behind the release
commit for the notes, and without it the release fails with
403 "Resource not accessible by integration".
Trusted publishing can only be configured for a crate that already exists on crates.io, so a brand-new crate needs a one-time manual bootstrap:
-
Add the crate under
crates/. Give it adescription; it inheritsversion/license/repository/authorsfrom[workspace.package]. If it should never be published (dev/test only), setpublish = false.If it is published, add it to
release-plz.tomlin two places — neither has a[workspace]-level form, and neither failure is loud:- its own
[[package]]entry withversion_group = "workspace", or it silently versions on its own, out of step with the rest; vihaco'schangelog_includelist, or its commits silently vanish from every future release note.
- its own
-
Publish it once by hand (logged in via
cargo login):cargo publish --manifest-path crates/<name>/Cargo.toml
If other new crates depend on it, publish in dependency order (see Manual release below).
-
Configure its trusted publisher on crates.io (see Auth).
After that, release-plz publishes it automatically with the rest.
The action is pinned by tag, matching release-plz's docs and the other version-pinned actions in this repo:
uses: release-plz/action@<tag> # e.g. v0.5.130Heads-up for automated edits: a name@version ref looks like an email
address, so editing this file through a tool that applies email obfuscation
(some web proxies, some AI assistants) can rewrite the ref into an obfuscated
placeholder. The result is an invalid uses: value, and GitHub then rejects the
entire workflow at startup — runs show 0 jobs and no logs, and the workflow
name falls back to the file path. If you edit the workflow with such a tool,
re-check the release-plz/action lines afterward.
Bump the version by changing the tag (or let Dependabot do it).
release-plz only processes crates it publishes. If git_tag_enable is ever moved
to a crate with publish = false — or to one that a given release does not touch
— no tag and no GitHub Release are created, and nothing fails to tell you so.
The run is green and the release simply has no tag.
version_group = "workspace" is what makes vihaco safe as the owner: every
published crate releases together, so vihaco is always in the released set.
Removing version_group from the crates re-opens this hole.
0.3.0 and earlier used per-crate tags (vihaco-v0.3.0, vihaco-parser-v0.3.0,
…); those 22 tags and their GitHub Releases are left in place as history. The
single-tag scheme applies to the first release cut after this change landed,
whatever version that turns out to be.
To bridge the two schemes, an annotated v0.3.0 tag was added at the same commit
the 0.3.0 tags point to (2d60335), so release-plz can resolve the previous
version under the new pattern and start the next changelog from the right place.
It has no GitHub Release attached.
Note that git_only is left at its default false, so release-plz determines
the previous version from crates.io, not from tags. The tags only bound the
changelog commit range.
Crates can't be deleted, only yanked (reversible):
cargo yank --version X.Y.Z <crate> # hide from new resolutions
cargo yank --version X.Y.Z <crate> --undo # reverse itNote that yanking a crate breaks any already-published crate that depends on that exact version (fresh installs can't resolve it). The fix is to ship a new patch release that repoints the dependents — exactly what the normal flow does.
If you ever need to release without the workflow, bump the version in the root
Cargo.toml ([workspace.package] and the [workspace.dependencies]
requirements), then publish in dependency order — each crate must be on the
index before the crates that depend on it:
vihaco-abi-derive → vihaco-abi → vihaco-bytecode → vihaco-module →
vihaco-runtime-derive → vihaco-runtime → vihaco-parser → vihaco-parser-derive →
vihaco-stdlib → vihaco-syntax → vihaco → vihaco-cpu
cargo publish --manifest-path crates/<name>/Cargo.tomlRecent cargo waits for each crate to appear in the index before returning, so
the next publish in the chain resolves cleanly.
Then tag and write the notes by hand, matching what the automation would have done — one tag for the whole workspace:
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
gh release create vX.Y.Z --title vX.Y.Z --notes-file <(…) # notes from CHANGELOG.md