Release Process

Praxis uses Semantic Versioning. The workspace version is the single source of truth, defined in workspace.package.version in the root Cargo.toml. All workspace crates inherit this version.
On this page

Versioning

Praxis uses Semantic Versioning. The workspace version is the single source of truth, defined in workspace.package.version in the root Cargo.toml. All workspace crates inherit this version.

Pre-release Checklist

Before tagging a release:

  • Lints are clean (make lint)
  • All tests pass locally (make test && make test-integration && make test-conformance)
  • Dependency audit passes (make audit)
  • Coverage meets the threshold (make coverage-check)
  • SemVer compliance checked locally with make semver (a manual step for now: the SemVer CI workflow runs manual-only, not on push or PR, until 1.x)
  • Benchmarks have been run; performance is similar or better than the previous release
  • Version in root Cargo.toml is bumped (both workspace.package.version and workspace.dependencies inter-crate versions)
  • Cargo.lock is regenerated with the new version
  • make publish-dry-run succeeds (for a dirty working tree, run cargo publish --workspace --dry-run --locked --allow-dirty directly, since the make target takes no extra flags)
  • The [Unreleased] section of CHANGELOG.md is rolled into the new version (see Changelog)

When the Tests, Tests (Integration), Conformance, Supply Chain, and Coverage workflows are not already green for the tagged commit, the release re-runs lint, the test suites, the dependency audit, and the coverage check before cutting a draft, so this checklist mainly catches problems before you push the tag. Other main-branch checks (MSRV, Documentation, Coding Conventions) are not re-run by the release, so tag a commit that has already passed them on main.

Tagging a Release

Tags follow the format v<MAJOR>.<MINOR>.<PATCH> (e.g. v0.1.0), optionally with a pre-release suffix (e.g. v1.0.0-rc.1), and must match workspace.package.version; the release workflow rejects mismatched tags. A pre-release tag cuts a pre-release draft. Push the tag to praxis-proxy/praxis itself, not to a fork (upstream below is whichever remote points there):

git tag v0.1.0
git push upstream v0.1.0

The release runs in two phases (.github/workflows/release.yaml).

Phase 1 runs on the tag push:

  1. Validate the tag against the workspace version
  2. Preflight: check whether the Tests, Tests (Integration), Conformance, Supply Chain, and Coverage workflows already concluded green for this commit
  3. When they are not all green, run lint, the test suites, the dependency audit, and the coverage check before continuing
  4. Verify every release crate packages cleanly (a publish dry run that build-verifies each crate)
  5. Build and publish the standard and FIPS container images to GHCR with the immutable :<version> and :sha-<hash> tags, -fips suffixed for the FIPS image (see FIPS 140-3)
  6. Cut a draft release (a pre-release draft for a pre-release tag) with generated notes

Phase 2 runs when a maintainer publishes the draft:

  1. Re-validate the tag against Cargo.toml, then publish the workspace to crates.io in one dependency-ordered run (make publish, which skips crates the index already has at this version, so a partial or repeated publish is safe to run again)
  2. For a stable (non pre-release) release, advance the moving :<major>.<minor> and :latest container tags, and their -fips counterparts

Review and edit the draft notes (see Changelog), then publish the release from the GitHub UI. Publishing the release is what performs the real crates.io publish (nothing reaches crates.io until you do), and it re-validates the tag against Cargo.toml before publishing. The crates job runs in a protected release GitHub Environment and authenticates with the RUST_CRATES_PUBLISH_TOKEN secret; crates.io OIDC trusted publishing is the recommended future replacement for that long-lived token.

Publishing Container Images

Container images are published to GitHub Container Registry (GHCR) by the release pipeline. Outside of a release, the Publish workflow (.github/workflows/publish.yaml) can be run manually via workflow_dispatch. It now actually builds and publishes the image (it was previously a no-op, gated behind a job condition that never held on a manual run). Use GitHub’s “Run workflow” ref selector to pick the branch or tag to build from; the workflow publishes whichever ref you dispatch it against (a branch dispatch tags the image with the branch name and :sha-<hash>, a tag dispatch with only :sha-<hash>).

Image Tags

Three workflows push image tags, each a different set. Each lists its tags inline and pins its actions by SHA. The release workflow (release.yaml) pushes the release tags across Phase 1 and Phase 2. The manual Publish workflow (publish.yaml) pushes only ref-identifying tags: the immutable :sha-<hash> plus the branch name. It never advances the moving :latest or :<major>.<minor> tags, so a manual run cannot repoint consumers. The Nightly workflow (nightly.yaml) pushes :nightly, a dated tag and :sha-<hash> once its test suites pass.

PatternExamplePushed
sha-<hash>sha-abc1234Phase 1 releases, manual Publish runs, Nightly
<version>0.1.0Phase 1, every release
<major>.<minor>0.1Phase 2, stable releases only
latestlatestPhase 2, stable releases only
<branch>mainManual Publish runs only
nightlynightlyNightly
nightly-<YYYYMMDD>nightly-20260923Scheduled Nightly runs
<any of the above>-fips0.1.0-fips, latest-fipsSame runs, for the FIPS image

Every run that pushes a standard image also pushes the FIPS image (Containerfile.fips, UBI 9) under the same tags with a -fips suffix, after make fips-verify-image confirms that Red Hat signed the pinned base images.

Phase 1 publishes only the immutable :<version> and :sha-<hash> tags. Phase 2 advances the moving :<major>.<minor> and :latest tags, and only after a stable (non pre-release) release is published, so they never point at a pre-release build. A pre-release therefore gets only :<version> and :sha-<hash>.

Phase 2 checks that both :<version> and :<version>-fips exist before it moves any tag. For a draft cut before the FIPS image existed, re-run Phase 1 before publishing the draft; once the release is published, Phase 1 no longer rebuilds its images.

Changelog

Praxis keeps a hand-edited CHANGELOG.md at the repository root under the org’s changelog convention: each PR with a user-visible change adds its entry under [Unreleased], and a release rolls that section into a heading for the new version.

Do the roll in the last commit before the tag. That isn’t always the version bump, because PRs often merge after it. The new section covers everything merged since the previous tag, so check it against git log --first-parent vPREV..<commit to tag>, and if other PRs landed after the roll, move their [Unreleased] entries into the new version too.

When you edit the release draft before publishing it, replace its generated “What’s Changed” list with the new version’s section from CHANGELOG.md. The generated “New Contributors” and “Full Changelog” lines can stay. The skip/changelog label only affects those generated notes, not CHANGELOG.md.

Release Branches

Release branches are optional and created from tags when backports are needed. The naming convention is release/v<MAJOR>.<MINOR>.x (e.g. release/v0.1.x).

Fixes are cherry-picked onto the release branch, a new patch tag is created from it, and the release workflow runs as usual (the tag push drives release.yaml).

Container Details

The standard image is a minimal Alpine container:

  • Dynamically linked musl build against Alpine’s OpenSSL (libcrypto3, libssl3), with LTO, single codegen unit, and stripped symbols
  • Runs as non-root user (praxis)
  • Exposes ports 8080 (proxy) and 9902 (health and metrics)
  • Built-in health check at http://127.0.0.1:9902/healthy
  • Config directory: /etc/praxis

The -fips image runs on ubi9/ubi-minimal instead, with the same user name, ports, health check URL and config directory; see FIPS 140-3.

Note: This is subject to change.