Python CI/CD Workflows
Generated by python-ci-library, which is composed by all three Python service archetypes:
python-rest-service-archetypepython-grpc-service-archetypepython-graphql-service-archetype
The protocol has no effect on the pipeline - all three render the same two workflow files.
Build Workflow
.github/workflows/build.yaml, shown as rendered for a project named billing-service:
name: Build
on:
push:
branches: ["**"]
pull_request:
permissions:
contents: write
id-token: write
env:
IMAGE_NAME: billing-service
APPLICATION_NAME: billing-service
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: p6m-actions/python-uv-setup@v1
- name: Login to Python Repository
uses: p6m-actions/python-uv-repository-login@v1
with:
credentials: |
artifactory=${{ secrets.P6M_ARTIFACTORY_USERNAME }}:${{ secrets.P6M_ARTIFACTORY_IDENTITY_TOKEN }}
- name: Cut Patch Version
if: github.ref_name == 'main'
id: cut-patch
uses: p6m-actions/python-uv-cut-tag@v1
with:
version-level: patch
- name: Build Python Application
uses: p6m-actions/python-uv-build@v1
# ... shared container build, release and dispatch steps
Step Chain
| # | Step | Scope | Runs on |
|---|---|---|---|
| 1 | Checkout | Shared | Always |
| 2 | uv setup | Python | Always |
| 3 | Login to Python Repository | Python only | Always |
| 4 | Cut Patch Version | Python | main |
| 5 | Build Python Application | Python | Always |
| 6 | Login to Artifactory Container Registry | Shared | main |
| 7 | Set up Docker Buildx | Shared | Always |
| 8 | Build and Publish Docker Image | Shared | Always |
| 9 | Make Artifacts | Shared | main |
| 10 | Create Github release | Shared | main |
| 11 | Update Application Manifest | Shared | main |
Python is the only language with a package registry login step, and it is the only one that runs a login on every branch rather than only on main.
Language-Specific Steps
uv Setup
- uses: p6m-actions/python-uv-setup@v1
p6m-actions/python-uv-setup installs uv and configures caching for uv virtual environments.
| Input | Default | Passed by the workflow |
|---|---|---|
version | latest | (default) - uv version |
python-version | (unset) | (default) - uv resolves the Python version from pyproject.toml / .python-version |
| Output | Description |
|---|---|
version | The uv version actually installed |
python-version | The Python version actually installed |
Login to Python Repository
- name: Login to Python Repository
uses: p6m-actions/python-uv-repository-login@v1
with:
credentials: |
artifactory=${{ secrets.P6M_ARTIFACTORY_USERNAME }}:${{ secrets.P6M_ARTIFACTORY_IDENTITY_TOKEN }}
p6m-actions/python-uv-repository-login configures uv's HTTP basic auth for private Python indexes.
| Input | Required | Format |
|---|---|---|
credentials | Yes | One registry=user:password mapping per line. registry is the index alias, which must match an index name declared in pyproject.toml |
Outputs: none. The effect is ambient uv credential configuration for the rest of the job.
Unlike the Docker registry login, this is a read credential - private dependencies have to resolve before anything compiles. Gating it on main would break every feature-branch build that depends on an internal package.
artifactory alias is a placeholder until you add a private indexA freshly generated project declares no [[tool.uv.index]] in pyproject.toml - all its dependencies come from PyPI - so the credentials configured here go unused. The step is pre-wired so that the day you add a private Artifactory index named artifactory, it resolves without a workflow change. If you name the index something else, change the alias on the left of the = to match.
Cut Patch Version
- name: Cut Patch Version
if: github.ref_name == 'main'
id: cut-patch
uses: p6m-actions/python-uv-cut-tag@v1
with:
version-level: patch
p6m-actions/python-uv-cut-tag bumps project.version in pyproject.toml, commits, and creates an annotated tag.
| Input | Required | Value |
|---|---|---|
version-level | No | patch in build.yaml; the dispatch input in cut-tag.yaml |
pyproject-path | No | pyproject.toml (default) |
| Output | Description |
|---|---|
version | The new version, e.g. 1.4.2 |
tag | The created tag |
The action starts with p6m-actions/token-exchange@v2, which trades the job's OIDC token for a P6M App installation token, configures git to commit as p6m-ybor[bot], and installs a prepare-commit-msg hook that prepends [skip ci] - preventing the bump commit from re-triggering the build.
Build Python Application
- name: Build Python Application
uses: p6m-actions/python-uv-build@v1
p6m-actions/python-uv-build runs uv sync unconditionally, then lint, test, and build in that order. No inputs are overridden, so all defaults apply:
| Input | Default | What runs |
|---|---|---|
| (none) | - | uv sync - resolves and installs into the project virtualenv, always |
run-lint | true | uv run ruff check, guarded |
run-test | true | uv run pytest, guarded |
run-build | true | uv build |
archive-coverage | false | Coverage under htmlcov is not uploaded |
Outputs: none.
The action does not blindly run its commands:
- Lint only runs when
pyproject.tomlcontains a[tool.ruff]section. The archetypes do not render one, so a freshly generated project logs "No ruff configuration found in pyproject.toml. Skipping linting." Adding[tool.ruff]to yourpyproject.tomlis what turns the gate on. - Tests run when
pyproject.tomlhas a[tool.pytest]section or anytest_*.py/*_test.pyfile exists. The archetypes rendertests/test_health.py, so tests do run - and a failure fails the build on every branch.
Cut Tag Workflow
.github/workflows/cut-tag.yaml:
name: Cut Tag
on:
workflow_dispatch:
inputs:
version-level:
description: "Version bump level"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
permissions:
id-token: write
contents: write
jobs:
cut-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: p6m-actions/python-uv-setup@v1
- uses: p6m-actions/python-uv-cut-tag@v1
with:
version-level: ${{ inputs.version-level }}
| Input | version-level - patch, minor, or major |
| Outputs | A [skip ci] version bump commit in pyproject.toml and an annotated tag, both pushed |
Version File
pyproject.toml is the source of truth:
[project]
name = "billing-service"
version = "1.4.2"
Related
- CI/CD Workflows - the shared steps and the CD handoff
- Python Actions - the wider uv action catalog
- Python Setup Guide - local development environment