Release Process
August 4, 2026 · View on GitHub
Version numbers follow SemVer, and what a given bump is allowed to break is defined in Stability & API Surface. Read that first if you are deciding between a patch and a minor.
Use the release scripts in two operator steps so the version bump goes through normal branch protection before the tag is published.
First create the release metadata PR:
make release-pr VERSION=v1.0.0
The script fetches origin/main, creates a release-v1.0.0 branch from it,
updates pyproject.toml and uv.lock, syncs .venv, commits the release
metadata with signoff, runs make validate-pr, pushes the branch, and opens the
pull request. On success, your checkout remains on the release PR branch. If a
step fails after that branch is created, the script prints local and remote
branch cleanup commands before retrying.
Review and merge that PR to main through the normal gate.
make release-pr leaves your checkout on the release PR branch, and the final
release requires local main to match the merged origin/main exactly. So
after the PR merges, switch back and fast-forward to the merge commit:
git switch main && git pull --ff-only origin main
Then run the final release from that clean, up-to-date checkout:
make release VERSION=v1.0.0
The full release flow:
- Requires a clean git worktree.
- Fetches
origin/mainand tags. - Requires the current branch to be
main. - Requires local
HEADto exactly match fetchedorigin/main. - Verifies the local tag, remote tag, and GitHub release do not already exist.
- Verifies
pyproject.tomlanduv.lockalready contain the target version. - Syncs
.venvand verifies installed package metadata so Control Center shows the release version. - Runs
make validate-pr. - Re-checks that the worktree is clean.
- Creates the annotated tag, pushes only that tag, and creates the GitHub release.
VERSION may be passed with or without the leading v; package metadata stores
the plain form (1.0.0) and release tags use the prefixed form (v1.0.0).
Pre-release tagging during 0.x
While the project is in 0.x, the public API is explicitly unstable, so every
release is published as a GitHub pre-release: step 10 runs
gh release create <tag> --generate-notes --prerelease. 0.x tags therefore
carry the pre-release badge and do not claim the "Latest" pointer, which stays
free until the first 1.0.0.
This is derived from the version itself — any 0.y.z tag is marked as a
pre-release, and 1.0.0 onward publishes as a normal release. There is no flag
to remember and no way for the real invocation to disagree with the dry-run
preview; both build the command through github_release_command() in
scripts/prepare_release.py.
SemVer pre-release identifiers (v0.11.0-beta.1) are not supported by the
release tooling today: normalize_release_version() requires a stable X.Y.Z
version so package metadata, uv.lock, and the tag cannot drift apart. Adding
them is a deliberate change to that validator, not something to work around.
Which surfaces 0.x lets a minor release break, and what must graduate before
the 0 is dropped, is documented in
Stability & API Surface.
The Control Center footer renders v{{ version }} from
importlib.metadata.version("issue-orchestrator") via
resolve_runtime_identity(). make release-pr refreshes uv.lock, runs
uv sync --frozen --all-extras, and verifies the local .venv metadata. The
final make release command repeats the environment sync and metadata
verification from the merged main commit so a restarted Control Center
displays the same release version in the sidebar footer.
To preview release PR creation without changing files:
make release-pr VERSION=v1.0.0 ARGS=--dry-run
To preview final release publishing without changing files:
make release VERSION=v1.0.0 ARGS=--dry-run
Dry-run still runs read-only preflight checks. For release-pr, that includes
the clean worktree check, origin/main lookup, branch and tag collision checks,
and optional GitHub release checks. For final release publishing, it also checks
the current branch, local HEAD versus remote origin/main, and merged release
metadata versions.
make prepare-release VERSION=v1.0.0 remains available as a lower-level file
prep command when you intentionally want to edit, commit, push, and open the PR
yourself.
If gh release create fails after the tag push, do not rerun the full release
command unchanged because the remote tag now exists. The failing run prints the
exact recovery command to stderr — copy that line rather than retyping one,
because it is built by the same github_release_command() that the release
itself uses and therefore carries the correct pre-release marking:
The v0.11.0 tag was pushed but the GitHub release was not created.
Do not rerun the release command; the remote tag already exists. Create the
release from the pushed tag with exactly:
gh release create v0.11.0 --generate-notes --prerelease
During 0.x the --prerelease flag is not optional: omitting it publishes the
release as a normal one and hands it the "Latest" pointer, contradicting the
0.x pre-release guarantee. From 1.0.0 onward the
printed command omits the flag.
For automation, use ARGS=--yes to skip the exact-tag confirmation prompt.