Tag and release python package with uv and GitHub Actions workflows

The goal of this article is to build and release python package using uv (for building and publishing) and github actions (for automation).

Build and publish

name: Package
on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:

jobs:
  build:
    name: "Build and publish package"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # for git-versionning
      - name: Install the latest version of uv
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
      - name: Build dist
        run: uv build
      - name: Publish packages
        run: uv publish --token ${{ secrets.TWINE_PASSWORD }}

[Optional] Deploy to server

jobs:
  deploy:
    needs:
      - build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to the server
        uses: https://github.com/appleboy/ssh-action@master
        with:
          host: ${{ vars.HOST_MACHINE }}
          username: ${{ secrets.HOST_SSH_USER }}
          key: ${{ secrets.HOST_SSH_KEY }}
          script: |
            pip install --force-reinstall <package>

[Optional] Custom PyPI Package Registry

If you are using custom PyPI Package Registry/Index (such as gitea), you can use uv.index entry in pyproject.toml:

[[tool.uv.index]]
name = "gitea"
url = "https://pypi.example.com/simple/"
publish-url = "https://gitea.example.com/api/packages/{owner}/pypi"
explicit = true

and then build with the following command by adding --index gitea:

uv --native-tls publish --index gitea --token ${{ secrets.TWINE_PASSWORD }}

[Optional] Fine-tune versioning

Using semver specification, you can fine-tune your release version.

- Static versioning

In pyproject.toml the version is statically defined as follow:

[project]
name = "your-project"
version = "0.1.0"

You can then get your version using those two commands:

$ uv version
your-project 0.1.0
# OR
$ uvx --from=toml-cli --native-tls toml get --toml-path=pyproject.toml project.version
0.1.0

You can bump version using uv version --bump minor for instance:

uv version --bump minor
Resolved 30 packages in 187ms
Audited 28 packages in 0.71ms
your-project 0.1.0 => 0.2.0

and pushing your updated pyproject.toml.

Using github actions, you can specify the version to install:

name: Package
on:
  workflow_dispatch:
  push:
    branches:
      - "master"

jobs:
  build:
    name: "Build and publish package"
    runs-on: ubuntu-latest
    outputs: ### <<<<< add this to pass version information between jobs
      version: ${{ steps.get_version.outputs.version }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # for git-versionning
      - name: Install the latest version of uv
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
      - name: Get version ### <<<<< add this to pass version information between jobs
        id: get_version
        run: |
          echo "version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)" >> $GITHUB_OUTPUT
      - name: Build dist
        run: uv build
      - name: Publish packages
        run: uv publish --token ${{ secrets.TWINE_PASSWORD }}
  deploy:
    needs:
      - build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to the server
        uses: https://github.com/appleboy/ssh-action@master
        with:
          host: ${{ vars.HOST_MACHINE }}
          username: ${{ secrets.HOST_SSH_USER }}
          key: ${{ secrets.HOST_SSH_KEY }}
          script: |
            pip install --force-reinstall your-project==${{ needs.build.outputs.version }}

- Dynamic versioning

In pyproject.toml add:

[project]
name = "your-project"
dynamic = ["version"]  # Remove static version and add this line

[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "uv-dynamic-versioning" # specify a version Source 

[tool.uv-dynamic-versioning]
fallback-version = "0.1.0" # (optional)

You can bump version by creating a git tag and the built package version will be based on git tag:

$ git tag v0.2.0
$ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist\your_project-0.2.0-py3-none-any.whl

Sources