Skip to main content

Python CI/CD Workflows

Generated by python-ci-library, which is composed by all three Python service archetypes:

  • python-rest-service-archetype
  • python-grpc-service-archetype
  • python-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

#StepScopeRuns on
1CheckoutSharedAlways
2uv setupPythonAlways
3Login to Python RepositoryPython onlyAlways
4Cut Patch VersionPythonmain
5Build Python ApplicationPythonAlways
6Login to Artifactory Container RegistrySharedmain
7Set up Docker BuildxSharedAlways
8Build and Publish Docker ImageSharedAlways
9Make ArtifactsSharedmain
10Create Github releaseSharedmain
11Update Application ManifestSharedmain

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.

InputDefaultPassed by the workflow
versionlatest(default) - uv version
python-version(unset)(default) - uv resolves the Python version from pyproject.toml / .python-version
OutputDescription
versionThe uv version actually installed
python-versionThe 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.

InputRequiredFormat
credentialsYesOne 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.

Why this runs on every branch

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.

The artifactory alias is a placeholder until you add a private index

A 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.

InputRequiredValue
version-levelNopatch in build.yaml; the dispatch input in cut-tag.yaml
pyproject-pathNopyproject.toml (default)
OutputDescription
versionThe new version, e.g. 1.4.2
tagThe 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:

InputDefaultWhat runs
(none)-uv sync - resolves and installs into the project virtualenv, always
run-linttrueuv run ruff check, guarded
run-testtrueuv run pytest, guarded
run-buildtrueuv build
archive-coveragefalseCoverage under htmlcov is not uploaded

Outputs: none.

Lint and test are guarded, and lint is currently a no-op

The action does not blindly run its commands:

  • Lint only runs when pyproject.toml contains 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 your pyproject.toml is what turns the gate on.
  • Tests run when pyproject.toml has a [tool.pytest] section or any test_*.py / *_test.py file exists. The archetypes render tests/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 }}
Inputversion-level - patch, minor, or major
OutputsA [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"