Release Process
llmenv follows semantic versioning and automates release distribution via GitHub Actions.
Overview​
Releases are triggered by pushing a v* tag. The release workflow:
- Builds binaries for macOS (arm64, x86_64) and Linux (x86_64, arm64; static musl builds)
- Generates SHA256 checksums and SLSA v1.0 provenance attestations
- Publishes to crates.io with trusted publishing (no stored token): the workspace crates
llmenv-util,llmenv-paths,llmenv-git, andllmenv-config, thenllmenv. A crate version that is already on crates.io is skipped, so a re-run is safe. - Creates a GitHub Release with binaries, checksums, SLSA provenance, and the changelog notes for the version in the release body
Changelog​
The changelog follows Keep a Changelog and Semantic Versioning. There is exactly one rule that matters and it is easy to get wrong:
A version section only exists once that version has been git-tagged. Until then, everything lives under
## [Unreleased].
There is one changelog file for each major version: CHANGELOG-<major>.md at the repo root.
website/docs/changelog.md is generated from them by scripts/sync-changelog-doc.sh.
The Cargo.toml version and the changelog must never run ahead of the tags.
If git tag -l shows no vX.Y.Z tag, there is no [X.Y.Z] changelog section
and Cargo.toml is not bumped to it. (This repo previously accumulated
phantom 1.0.0/1.1.0/1.2.0 sections with no tags behind them — don't
recreate that.)
While developing (every change)​
Add an entry under ## [Unreleased] in the appropriate category. Do not
touch the Cargo.toml version and do not create a new version heading.
Categories (Keep a Changelog): Added, Changed, Deprecated, Removed,
Fixed, Security. This repo also uses Documentation. Reference the issue/PR
number in the entry, e.g. (#63).
## [Unreleased]
### Added
- New `llmenv foo` subcommand that does X. (#81)
When cutting a release (and only then)​
Use cargo-release to prepare the
bump. It atomically renames [Unreleased] → [X.Y.Z] - YYYY-MM-DD, re-seeds a
fresh [Unreleased], bumps Cargo.toml, updates Cargo.lock, and makes a
single chore(release): X.Y.Z commit.
cargo release <patch|minor|major> # dry-run preview (default)
cargo release <patch|minor|major> --execute # apply
It is configured (in release.toml) to not tag, publish, or push — all
three happen in the steps below. Full details in
RELEASING.md.
Version Tags​
main is PR-protected, so the version-bump commit lands through a PR first.
After merge, tag the merged commit and push the tag — that is what fires the
release workflow.
# 1. Create a prep branch, run cargo-release, open a PR, merge it.
git switch main && git pull
# 2. Tag the merged commit and push the tag.
git tag -a "vX.Y.Z" -m "vX.Y.Z"
git push origin "vX.Y.Z"
The release workflow triggers on any v* tag push. A release of the newest major
is tagged on main; a patch or minor release of an older major is tagged on its
release/X.x branch (see Branch Strategy). The workflow has no
branch filter, so a tag pointed at a stale commit would still fire — make sure
you're tagging the merged result.
Binary Distribution​
GitHub Releases​
Pre-built binaries are attached to each release on GitHub:
llmenv-linux-x86_64— Linux x86_64llmenv-linux-aarch64— Linux arm64llmenv-macos-x86_64— macOS Intel (x86_64)llmenv-macos-aarch64— macOS Apple Silicon (arm64)checksums.txt— SHA256 checksums for all binaries*.intoto.jsonl— SLSA v1.0 provenance attestations (one per binary)
Verify Binary Integrity​
Each release includes checksums.txt. Verify downloaded binaries:
sha256sum -c checksums.txt
Verify Supply Chain (SLSA)​
Each binary ships with a SLSA v1.0 provenance attestation. Verify that the binary was built by GitHub Actions from the claimed source:
slsa-verifier verify-artifact \
--artifact-path=<binary> \
--provenance=<provenance.intoto.jsonl> \
--source-uri=github.com/phaedrus1992/llmenv
crates.io​
The Rust crate is published to crates.io. Install with:
cargo install llmenv
Prerequisites:
- Each of the five crates has a trusted publisher on crates.io that names the
phaedrus1992/llmenvrepo and therelease.ymlworkflow. - The workflow gets a short-lived token from crates.io for each run. No
CARGO_REGISTRY_TOKENsecret is stored.
Homebrew​
A Homebrew tap is maintained at phaedrus1992/homebrew-tap.
Install:
brew install phaedrus1992/tap/llmenv
Update:
brew upgrade llmenv
Maintenance​
Adding a new platform​
To add a new platform (e.g., Windows, aarch64 Linux):
-
Update
.github/workflows/release.yml:- Add a new matrix entry under
build-binaries - Set
os,target,asset_name
- Add a new matrix entry under
-
Test locally:
rustup target add <target>cargo build --release --target <target> -
Update Homebrew formula if a new macOS target is added:
- Modify Formula/llmenv.rb in
phaedrus1992/homebrew-tap - Add conditional blocks for the new architecture
- Modify Formula/llmenv.rb in
Rollback​
If a release needs to be pulled:
# Mark as pre-release on GitHub (manual UI)
# Unpublish from crates.io (requires crates.io owner access)
cargo yank --vers X.Y.Z
# Remove from Homebrew (PR to phaedrus1992/homebrew-tap)
Secrets Configuration​
The release workflow requires one secret (repo Settings → Secrets and variables → Actions):
HOMEBREW_TAP_TOKEN— GitHub PAT with write access tophaedrus1992/homebrew-tap
crates.io needs no secret, because the workflow uses trusted publishing.
The forward-merge-release workflow also reads an optional FORWARD_MERGE_PAT secret (see Branch Strategy).
Security notes:
- The Homebrew token is passed via environment variable (never command-line arguments)
- GitHub Actions automatically masks secret values in logs
- Always use fine-grained tokens with minimal scope (publish-only)
Branch Strategy​
Feature development happens on main.
Each major version gets one long-lived release/X.x branch, created from the first release tag of that major.
Bug fixes and small enhancements for that major land there, and the same branch hosts every patch and minor release of that major (3.11.2, 3.12.0).
Backport policy — fixes are applied (when feasible) to:
| Branch | Description |
|---|---|
release/X.x | Current major — always patched |
release/(X-1).x | Previous major — always patched |
Fix in the oldest applicable branch first.
The forward-merge-release workflow then carries the fix, with its changelog entry, into every newer branch.
Do not cherry-pick it by hand.
Full workflow in RELEASING.md.
Forward-merge workflow​
The forward-merge-release workflow runs on every push to a release/X.x branch.
It merges the branch into the next release branch, and that branch into the next one, and so on up to main.
Each link of the chain merges the previous link, so main also receives the commits of the branches in between.
- A target that already holds the source is skipped.
- Without the
FORWARD_MERGE_PATsecret, the workflow pushes the merge to the target branch. If the push is rejected, it opens a pull request and stops. - With
FORWARD_MERGE_PATset, the workflow never pushes to a target. It always opens aforward-merge/<source>-to-<target>pull request, so the required checks run first. When someone merges that pull request, the workflow starts again from the same source, so the rest of the chain is merged. - A merge that conflicts is resolved by the workflow only for three kinds of file.
It regenerates
website/docs/changelog.mdand the two lockfiles (Cargo.lock,website/package-lock.json). It keeps the target's own version in theCargo.tomlfiles, when the source changed nothing else there. - Any other conflict stops the chain and fails the run with the
git mergecommand to run by hand. Resolve it on the target branch, then push. - A
forward-merge/<source>-to-<target>branch that is left behind is updated with each new source commit. If that update conflicts, the branch has no open pull request, and only the workflow wrote its commits, the workflow resets the branch to a fresh merge and logs the old tip. A branch with a commit from a person, or with an open pull request, is never reset: the run stops as above.
Troubleshooting​
Release workflow doesn't trigger​
- Verify the tag was pushed:
git push origin "vX.Y.Z" - Check GitHub Actions tab for the workflow run
- Confirm the tag format matches
v*(the workflow trigger pattern)
Publish fails with "unauthorized"​
- Check that each crate on crates.io has a trusted publisher for the
release.ymlworkflow - Check the
Get crates.io tokenstep of thepublish-cratejob
Binary artifacts missing from release​
- Check the
build-binariesjob succeeded in Actions tab - Verify artifact paths match in
create-release - Checksums should always be generated automatically
Checksum verification fails​
- Ensure you're on the same system/shell as the release CI
- Download the binary and checksums.txt from the same release
- Run
sha256sum -c checksums.txtin the download directory