Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

LabelSection
breaking-change, semver:majorBreaking Changes
feature, enhancement, featFeatures
bug, fixBug Fixes
performance, perfPerformance
documentation, docsDocumentation
(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 (with allow-dirty = ["ci"] set so our edits survive dist init: --generate-notes, and the Homebrew job’s fluree-beta formula). Leave dist’s publish-prereleases off: it would publish a pre-release as fluree itself.
  • .github/workflows/python-release.yml — the Python wheels. It publishes through PyPI trusted publishing: the fluree project on PyPI trusts this workflow running in the repository’s pypi environment, so no PyPI token is stored. That environment’s deployment rule admits only v* tags; it is what stops any other ref from reaching PyPI, so keep it when editing the repository’s environments. Its build runners match dist-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 next with PRs that belong to the major. CI runs on them as it does for main.
  • Merge main into next regularly so the final merge stays small. Use a merge, not a rebase: next is shared.
  • Land the major with a merge commit. When next merges into main, choose Create a merge commit — never squash or rebase. That keeps every commit and PR merged into next in main’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 into next as you would PRs into main.
  • Closing keywords don’t fire on next. See Issue linking.
  • Tags trigger releases from any branch (above). Don’t tag next unless you mean to publish a prerelease from it.