Releasing
Cutting a release is two phases: a release PR that bumps the version, then a tag pushed after the PR merges. The tag triggers cargo-dist, which builds binaries and publishes the GitHub Release. Release notes are auto-generated by GitHub from merged PR titles since the previous tag.
The flow
# 1. Branch off main and bump the workspace version.
git checkout main && git pull
git checkout -b release/v4.0.3
$EDITOR Cargo.toml # update [workspace.package].version
cargo update --workspace # refresh Cargo.lock
git commit -am "release v4.0.3"
git push -u origin release/v4.0.3
gh pr create --title "release v4.0.3"
# 2. After the PR is reviewed and merged to main, tag the merge commit.
git checkout main && git pull
git tag v4.0.3
git push origin v4.0.3 # ← triggers .github/workflows/release.yml
Watch the Actions tab. cargo-dist builds platform binaries, creates the GitHub Release with auto-generated notes, publishes the Homebrew formulae, and pushes the multi-arch Docker image.
The tap (fluree/homebrew-tap) has two formulae. fluree follows stable releases. fluree-beta follows the newest release of either kind, and is keg-only so it installs beside fluree. A pre-release tag (v4.3.0-beta.1, including one on next) updates only fluree-beta; a stable tag updates both. A formula never moves backwards: re-running an older release’s workflow, or a stable 4.x after a 5.0.0-beta, leaves the newer formula in place. Docker and AUR publish stable releases only.
The same tag runs .github/workflows/python-release.yml, which builds the fluree Python wheels for the CLI’s platforms, tests each one installed into a fresh environment, and publishes them to PyPI under the same version (a -rc.1 tag becomes PyPI pre-release rc1). It refuses to publish when the tag and the version in Cargo.toml differ, so tag the commit that bumped it. Run that workflow by hand from the Actions tab to build and test wheels without publishing — worth doing before a release that changes the Python package or its dependencies.
Why the two-phase split
cargo-dist’s release workflow triggers on any pushed vX.Y.Z tag regardless of which branch the tag points at. Holding the tag step until after merge ensures every release goes through PR review.
How release notes are generated
.github/workflows/release.yml calls gh release create --generate-notes. GitHub builds the release body automatically from PRs merged since the previous tag, categorized per .github/release.yml:
| Label | Section |
|---|---|
breaking-change, semver:major | Breaking Changes |
feature, enhancement, feat | Features |
bug, fix | Bug Fixes |
performance, perf | Performance |
documentation, docs | Documentation |
| (anything else) | Other Changes |
Apply one of these labels to each PR before merging. Unlabeled PRs still appear under “Other Changes” with their full title and author credit, so categorization is nice-to-have, not required.
PR titles already follow the feat: / fix: / docs: convention from CLAUDE.md, which makes the unlabeled “Other Changes” list readable on its own.
Rolling back
During Phase 1, before pushing:
git checkout main
git branch -D release/vX.Y.Z
After the release PR is opened but before merge:
Close the PR and delete the branch on GitHub. Nothing has shipped.
After the tag is pushed but cargo-dist still running:
git push origin :refs/tags/vX.Y.Z # delete the remote tag
git tag -d vX.Y.Z # delete the local tag
Cancel the in-progress Release and Python release workflow runs from the Actions tab.
After the Python wheels reached PyPI:
PyPI never accepts the same version twice, even after the release is deleted. Yank the release on pypi.org (installs that pin the version keep working; others skip it) and ship the fix as the next patch version.
After cargo-dist created the GitHub Release:
Delete the GitHub Release from the UI, then delete the tag (commands above). The merge commit on main stays in place — re-tag it once the issue is fixed, or supersede it with another release PR.
Configuration files
.github/release.yml— categorization for GitHub’s auto-generated release notes.dist-workspace.toml— cargo-dist’s distribution targets and installers..github/workflows/release.yml— autogenerated by cargo-dist (withallow-dirty = ["ci"]set so our edits survivedist init:--generate-notes, and the Homebrew job’sfluree-betaformula). Leave dist’spublish-prereleasesoff: it would publish a pre-release asflureeitself..github/workflows/python-release.yml— the Python wheels. It publishes through PyPI trusted publishing: theflureeproject on PyPI trusts this workflow running in the repository’spypienvironment, so no PyPI token is stored. That environment’s deployment rule admits onlyv*tags; it is what stops any other ref from reaching PyPI, so keep it when editing the repository’s environments. Its build runners matchdist-workspace.toml’s; keep the two in step when a platform is added.
The next branch
Work for the next major version — index format changes and anything else that needs a reindex or
breaks compatibility — lands on next, not main. main keeps shipping patch and minor releases.
- Target
nextwith PRs that belong to the major. CI runs on them as it does formain. - Merge
mainintonextregularly so the final merge stays small. Use a merge, not a rebase:nextis shared. - Land the major with a merge commit. When
nextmerges intomain, choose Create a merge commit — never squash or rebase. That keeps every commit and PR merged intonextinmain’s history, and GitHub’s generated release notes list those PRs individually (they pick up any PR whose commits fall in the tag range, whatever branch it targeted). Label PRs intonextas you would PRs intomain. - Closing keywords don’t fire on
next. See Issue linking. - Tags trigger releases from any branch (above). Don’t tag
nextunless you mean to publish a prerelease from it.