Skip to content

Release Pipeline

One command detects changes, builds on the right runners, creates GitHub releases, publishes to PyPI, bumps versions, and pushes. All planned locally, executed on CI.

The plan

The plan encodes everything CI needs.

FieldPurpose
build_matrixUnique runner sets (drives CI strategy.matrix)
python_versionPython version for CI (default "3.12")
publish_environmentGitHub Actions environment for trusted publishing
skipJob names to skip
reuse_runWorkflow run ID to reuse artifacts from
reuse_releasesWhether to reuse existing GitHub releases
jobsOrdered list of Job objects with commands
changesDetected package changes

The plan is serialized as JSON and passed via gh workflow run release.yml -f plan=<json>.

Version state space

A package's version in pyproject.toml follows PEP 440. uvr recognizes 11 distinct version forms via the VersionState enum. Each form determines which release types are valid, how baselines are resolved, and what the post-release bump looks like.

VersionStateExampleDescription
CLEAN_STABLE1.2.3Released stable version (transient)
DEV0_STABLE1.2.3.dev0Start of development toward 1.2.3
DEVK_STABLE1.2.3.dev3After dev releases toward 1.2.3
CLEAN_PRE01.2.3a0First pre-release of a kind (transient)
CLEAN_PREN1.2.3a2Subsequent pre-release (transient)
DEV0_PRE1.2.3a1.dev0Start of development toward 1.2.3a1
DEVK_PRE1.2.3a1.dev3After dev releases toward 1.2.3a1
CLEAN_POST01.2.3.post0First post-release (transient)
CLEAN_POSTM1.2.3.post2Subsequent post-release (transient)
DEV0_POST1.2.3.post0.dev0Start of development toward 1.2.3.post0
DEVK_POST1.2.3.post0.dev3After dev releases toward 1.2.3.post0

"Transient" forms exist briefly during the release pipeline between the "set release versions" commit and the "prepare next release" bump commit. The "dev" forms are the at-rest states that developers see during normal work.

The distinction between DEV0 and DEVK matters for baseline resolution. DEV0 means no dev releases have been published yet in this cycle. DEVK (where K > 0) means at least one dev release was published, which shifts the dev number forward.

Stable release cycle

The most common path. Development happens at .dev0, release strips the suffix, and the bump phase advances to the next patch.

Pre-release cycle

Enter a pre-release track by setting an explicit version with uvr version --set 1.0.0a0.dev0, iterate with uvr release (auto-detected as pre-release from the version string), advance to the next kind with another --set, and exit to stable with uvr version --bump stable followed by uvr release.

Post-release cycle

Post-releases fix a published stable version without bumping the version number. Enter with uvr version --bump post from a clean final version.

Post-release versions cannot enter pre-release and vice versa. These are separate tracks from a given stable version.

Dev release cycle

Dev releases publish the .devN version as-is rather than stripping it. The bump phase increments the dev number instead of the patch.

Dev releases can happen from any .dev version. A stable release from .devN strips the suffix and publishes the underlying version.

Release version transformation

How the current version maps to release_version and next_version for each release type.

Current VersionRelease TypeRelease VersionNext Version
1.0.0.dev0stable1.0.01.0.1.dev0
1.0.0.dev3stable1.0.01.0.1.dev0
1.0.0.dev0dev1.0.0.dev01.0.0.dev1
1.0.0.dev3dev1.0.0.dev31.0.0.dev4
1.0.0a0.dev0pre1.0.0a01.0.0a1.dev0
1.0.0a2.dev0stable1.0.01.0.1.dev0
1.0.0.post0.dev0post1.0.0.post01.0.0.post1.dev0

Local phase

StepWhat happens
Scan workspaceRead [tool.uv.workspace].members, apply include/exclude
Resolve baselinesCall _find_baseline_tag() per package
Detect changesTree OID comparison + transitive BFS propagation
Compute versionsCurrent version to release version to next version
Generate release notesCommit log between baseline and HEAD for each changed package
Check tag conflictsVerify no planned tags already exist in the repo
Commit version pinsWrite release versions + dep pins, commit
Push + dispatchgit push, then gh workflow run release.yml -f plan=<json>

If --dry-run is passed, everything through "Generate release notes" runs but no commits, pushes, or dispatches happen.

CI phase

validate

Always runs. Cannot be skipped. Confirms the plan schema version matches the deployed uvr.

build

One CI job per unique runner. Each job runs topologically layered builds and uploads wheels as wheels-<runner-labels>. See Build System for details on layer assignment and build ordering.

release

Downloads all wheels-* artifacts and creates one GitHub release per changed package. Each release gets a tag ({name}/v{version}), release notes, and attached wheels. The [tool.uvr.config].latest setting controls the "Latest" badge.

publish

Optional. Runs uv publish per changed package. The environment field enables trusted publishing via OIDC between your GitHub repo and PyPI. No API tokens needed.

bump

The only CI job that writes to the repository.

  1. Bump to next dev version
  2. Pin internal deps to just-published versions
  3. Sync lockfile, commit, tag baselines, push

Failure modes

When a CI job fails, the pipeline stops and leaves the system in a partial state.

Recovery commands

Failure PointSystem StateRecovery Command
build failsVersion commit pushed. No tags. No wheels.Re-run the workflow or revert and start over.
release failsWheels exist in CI artifacts. No tags.uvr release --skip build --reuse-run <RUN_ID>
publish failsRelease tags and GitHub releases exist.uvr release --skip build --skip release
bump failsEverything published. Repo not bumped.uvr release --skip build --skip release --skip publish

The --reuse-run flag tells the build phase to download wheels from the specified CI run's artifacts instead of building from scratch.

When release is skipped, release tag conflict checks are suppressed because the tags already exist from the previous run.

Tag conflict detection

Before generating a plan, the planner checks whether any planned tags already exist in the local repo.

Release tags ({name}/v{release_version}) are checked unless release is in the skip list. Skipping release means the tags already exist from a previous successful run.

Baseline tags ({name}/v{next_version}-base) are always checked.

If conflicts are found, the planner exits with suggestions.

  1. Use --bump post to publish a post-release instead
  2. Bump past the conflict with uvr version

Version conflict detection

Separately from tag conflicts, the planner checks whether any package's dev version targets a version that was already released. For example, if pyproject.toml says 1.0.1a1.dev0 but the tag pkg/v1.0.1a1 already exists, that version was already published and should not be developed toward again.

The resolution is to bump past the conflict with uvr version.