uvr workflow: A CI Template You Never Debug
uvr's workflow YAML never changes when your repo structure changes. Add packages, change dependency graphs, modify build matrices. The YAML stays the same. All intelligence is in the plan JSON, and the workflow just reads it.
# This drives the entire build matrix.
# Works for 1 package or 100, 1 runner or 10.
strategy:
matrix:
runner: ${{ fromJSON(inputs.plan).build_matrix }}The ReleaseWorkflow model
A Pydantic model defines the expected schema for release.yml.
- Generation.
uvr workflow initinstantiatesReleaseWorkflow()with defaults and serializes to YAML. - Validation.
uvr workflow validateloads the YAML and runs all validators including frozen field checks. - Documentation. The model is the single source of truth for what the workflow should look like.
All jobs inherit from Job with extra="allow". You can add arbitrary keys (permissions, outputs, concurrency) without breaking validation.
Frozen fields
Core jobs contain fromJSON(inputs.plan) expressions that CI depends on. Changing them silently breaks the pipeline. uvr marks them frozen. Validation warns (but doesn't block) if they're modified.
| Job | Frozen fields |
|---|---|
BuildJob | if, strategy, runs-on, steps |
ReleaseJob | if, strategy, steps |
BumpJob | if, steps |
Pipeline enforcement
Core jobs have mandatory needs dependencies.
uvr-validate → uvr-build → uvr-release → uvr-bumpIf you remove a required needs entry, the _needs_validator silently adds it back. You can add extra entries (e.g., needs: [uvr-build, my-test-job]) but can't remove the required ones.
Custom workflow jobs
Add your own jobs to release.yml by editing the YAML directly. WorkflowJobs uses extra="allow", so any additional jobs pass validation.
pre-build:
needs: [uvr-validate]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: uv run poe check
- run: uv run poe testCustom jobs survive uvr workflow init --upgrade. The three-way merge preserves user additions.
Python hooks
For injecting data into the plan before it reaches CI.
from uv_release import ReleaseHook, ReleasePlan
class Hook(ReleaseHook):
def post_plan(self, plan: ReleasePlan) -> ReleasePlan:
data = plan.model_dump()
data["deploy_env"] = "staging"
return ReleasePlan.model_validate(data)Local hooks run on your machine during uvr release. pre_plan, post_plan.
CI hooks run inside the workflow. pre_build, post_build, pre_build_stage, post_build_stage, pre_build_package, post_build_package, pre_release, post_release, pre_publish, post_publish, pre_bump, post_bump.
Init, validate, and upgrade
uvr workflow init
Generates release.yml from defaults. Checks that the CWD is a git repo with [tool.uv.workspace].members. Refuses to overwrite without --force.
uvr workflow validate
Validates the existing YAML against the model. Reports frozen field warnings and errors. Never modifies the file.
uvr workflow init --upgrade
Performs a three-way merge between the merge base (defaults from the uvr version that generated the file), the current file (your customized version), and the new defaults (from the current uvr version).
Your customizations are preserved. Conflicts open in your editor. Use --base-only to inspect merge bases without merging.