Ultraviolet (uv) Integration¶
uv is an extremely fast Python package and project manager that provides a modern alternative to pip and venv. It provides a lot of features that solve the common problems of Python package management but it also introduces a few quirks that need to be taken into account when using Python Semantic Release.
Important
Prerequisite: Make sure you have run through the Getting Started Guide before proceeding with this guide.
Updating the uv.lock¶
One of the best features of uv is that it automatically generates a lock file
(uv.lock) that contains the exact versions of all the dependencies used in
your project. The lock file is generated when you run the uv install command,
and it is used to ensure that CI workflows are repeatable and development environments
are consistent.
When creating a new release using Python Semantic Release, PSR will update the version
in the project’s definition file (e.g., pyproject.toml) to indicate the new version.
Unfortunately, this action will cause uv to fail on the next execution because the
lock file will be out of sync with the project’s definition file. There are two ways to
resolve this issue depending on your preference:
Add a step to your build command: Modify your semantic_release.build_command to include the command to update the lock file and stage it for commit. This is commonly used with the GitHub Action and other CI/CD tools when you are building the artifact at the time of release.
[tool.semantic_release] build_command = """ uv lock --upgrade-package "$PACKAGE_NAME" git add uv.lock uv build """
The intent of the lock upgrade-package call is ONLY to update the version of your project within the lock file after PSR has updated the version in your project’s definition file (e.g.,
pyproject.toml). When you are running PSR, you have already tested the project as is and you don’t want to actually update the dependencies if a new one just became available.For ease of use, PSR provides the
$PACKAGE_NAMEenvironment variable that contains the name of your package from the project’s definition file (pyproject.toml:project.name).If you are using the PSR GitHub Action, you will need to add an installation command for
uvto the build_command because the action runs in a Docker environment does not includeuvby default. The best way to ensure that the correct version ofuvis installed is to define the version ofuvin an optional dependency list (e.g.build). This will also help with other automated tools like Dependabot or Renovate to keep the version ofuvup to date.[project.optional-dependencies] build = ["uv ~= 0.7.12"] [tool.semantic_release] build_command = """ python -m pip install -e '.[build]' uv lock --upgrade-package "$PACKAGE_NAME" git add uv.lock uv build """
Stamp the code first & then separately run release: If you prefer to not modify the build command, then you will need to run the
uv lock --upgrade-package <your-package-name>command prior to actually creating the release. Essentially, you will run PSR twice: (1) once to update the version in the project’s definition file, and (2) a second time to generate the release.The intent of the
uv lock --upgrade-package <your-package-name>command is ONLY to update the version of your project within the lock file after PSR has updated the version in your project’s definition file (e.g.,pyproject.toml). When you are running PSR, you have already tested the project as is and you don’t want to actually update the dependencies if a new one just became available.# 1. PSR stamps version into files (nothing else) # don't build the changelog (especially in update mode) semantic-release -v version --skip-build --no-commit --no-tag --no-changelog # 2. run UV lock as pyproject.toml is updated with the next version uv lock --upgrade-package <your-package-name> # 3. stage the lock file to ensure it is included in the PSR commit git add uv.lock # 4. run PSR fully to create release semantic-release -v version
Advanced Example
Of course, you can mix and match these 2 approaches as needed. If PSR’s pipeline was using
uv, we would have a mixture of the 2 approaches because we run the build in a separate
job from the release. In our case, PSR would also need to carry the lock file as a workflow
artifact along the pipeline for the release job to commit it. This advanced workflow would
look like this:
# File: .tool-versions
uv 0.7.12
# File: .python-version
3.11.11
# File: pyproject.toml
[project.optional-dependencies]
build = ["python-semantic-release ~= 10.0"]
test = ["pytest ~= 8.0"]
[tool.semantic_release]
build_command = """
uv lock --upgrade-package "$PACKAGE_NAME"
uv build
"""
# File: .github/workflows/release.yml
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
env:
dist_artifacts_name: dist
dist_artifacts_dir: dist
lock_file_artifact: uv.lock
steps:
- name: Setup | Checkout Repository at workflow sha
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.sha }}
- name: Setup | Force correct release branch on workflow sha
run: git checkout -B ${{ github.ref_name }}
- name: Setup | Install uv
uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2
- name: Setup | Install Python & Project dependencies
run: uv sync --extra build
- name: Build | Build next version artifacts
id: version
env:
GH_TOKEN: "none"
run: uv run semantic-release -v version --no-changelog --no-commit --no-tag
- name: Upload | Distribution Artifacts
if: ${{ steps.version.outputs.released == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ env.dist_artifacts_name }}
path: ${{ format('{0}/**', env.dist_artifacts_dir) }}
if-no-files-found: error
retention-days: 2
- name: Upload | Lock File Artifact
if: ${{ steps.version.outputs.released == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ env.lock_file_artifact }}
path: ${{ env.lock_file_artifact }}
if-no-files-found: error
retention-days: 2
outputs:
new-release-detected: ${{ steps.version.outputs.released }}
new-release-version: ${{ steps.version.outputs.version }}
new-release-tag: ${{ steps.version.outputs.tag }}
new-release-is-prerelease: ${{ steps.version.outputs.is_prerelease }}
distribution-artifacts: ${{ env.dist_artifacts_name }}
lock-file-artifact: ${{ env.lock_file_artifact }}
test-e2e:
needs: build
runs-on: ubuntu-latest
steps:
- name: Setup | Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Setup | Download Distribution Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: ${{ needs.build.outputs.new-release-detected == 'true' }}
id: artifact-download
with:
name: ${{ needs.build.outputs.distribution-artifacts }}
path: ./dist
- name: Setup | Install uv
uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2
- name: Setup | Install Python & Project dependencies
run: uv sync --extra test
- name: Setup | Install distribution artifact
if: ${{ steps.artifact-download.outcome == 'success' }}
run: |
uv pip uninstall my-package
uv pip install dist/python_semantic_release-*.whl
- name: Test | Run pytest
run: uv run pytest -vv tests/e2e
release:
runs-on: ubuntu-latest
needs:
- build
- test-e2e
if: ${{ needs.build.outputs.new-release-detected == 'true' }}
concurrency:
group: ${{ github.workflow }}-release-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
steps:
- name: Setup | Checkout Repository on Release Branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.ref_name }}
- name: Setup | Force release branch to be at workflow sha
run: git reset --hard ${{ github.sha }}
- name: Setup | Install uv
uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2
- name: Setup | Install Python & Project dependencies
run: uv sync --extra build
- name: Setup | Download Build Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
id: artifact-download
with:
name: ${{ needs.build.outputs.distribution-artifacts }}
path: dist
- name: Setup | Download Lock File Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ needs.build.outputs.lock-file-artifact }}
- name: Setup | Stage Lock File for Version Commit
run: git add uv.lock
- name: Release | Create Release
id: release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
uv run semantic-release -v --strict version --skip-build
uv run semantic-release publish
outputs:
released: ${{ steps.release.outputs.released }}
new-release-version: ${{ steps.release.outputs.version }}
new-release-tag: ${{ steps.release.outputs.tag }}
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.released == 'true' && github.repository == 'python-semantic-release/my-package' }}
needs:
- build
- release
environment:
name: pypi
url: https://pypi.org/project/my-package/
permissions:
id-token: write
steps:
- name: Setup | Download Build Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
id: artifact-download
with:
name: ${{ needs.build.outputs.distribution-artifacts }}
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
packages-dir: dist
print-hash: true
verbose: true