Releases¶
Releases are cut by a manual GitHub Actions run and versioned automatically from conventional-commit history. This page covers how the next version is decided, how a maintainer triggers a release, and the job chain that builds, tests, and publishes it. Read contributing first for why PR titles are conventional commits — that is the input this whole pipeline consumes.
How the version is decided¶
python-semantic-release (PSR, v10) computes the next version from the conventional-commit messages on main since the last tag. Because the repo is squash-merged, each of those messages is a PR title. The mapping ([tool.semantic_release] in pyproject.toml):
| PR title | Bump | Example |
|---|---|---|
feat: |
minor | 0.3.1 → 0.4.0 |
fix: / perf: |
patch | 0.3.1 → 0.3.2 |
a breaking change (feat!: or a BREAKING CHANGE: footer) |
minor — stays 0.x | 0.3.1 → 0.4.0 |
docs: / refactor: / chore: / style: / test: / build: / ci: |
none | no release |
Two config choices shape this:
major_on_zero = falsewithallow_zero_version = truekeeps the package on 0.x: while the major is 0, a breaking change bumps the minor instead of going to 1.0. 1.0 is cut deliberately, never by a commit — the criteria live in versioning.parse_squash_commits = falsemeans PSR treats each commit message atomically, so the squash PR title alone decides the bump. Body bullets and branch commits contribute nothing: afeat:buried in a PR body under achore:title ships no feature bump.
The version itself is single-sourced in dlt_ops/__init__.py::__version__ (hatchling reads it for the build), so PSR stamps only that one file and never writes a [project].version key. It sits at 0.0.0 until the first release.
Triggering a release¶
The Semantic Release workflow runs only on workflow_dispatch — a manual run from the Actions tab — never on push. It takes two inputs:
force_level—none(the default; derive the bump from commits) orpatch/minor/majorto override.noneis passed to PSR as an empty string; the other three force that level.prerelease— cut the next version as a prerelease.
The first release is handled automatically: with no tags in the repo, PSR computes from the full history and lands 0.1.0 given a feat: commit.
The job chain¶
Five jobs run in sequence; a failure at any stage stops the release before anything is published.
verify-cirefuses to release off a redmain. It polls the latest CI run onmainand proceeds only if that run concludedsuccess; an in-progress run gets up to 30 minutes before the job gives up. No green CI, no release.releasemints a GitHub App installation token (scoped tocontents: write), checks out full history with it, and runs PSR: compute the version, stampdlt_ops/__init__.py, updateCHANGELOG.md, commitchore(release): X.Y.Z, tagvX.Y.Z, and create the GitHub Release. The wheel and sdist are built inside the PSR action container (build_command = "python -m pip install uv && uv build"); a follow-up step attaches them to the release, and they are uploaded as adistartifact for the later jobs. It runs as a GitHub App rather than the default token because the release commit and tag must land onmainpast branch protection, which the defaultGITHUB_TOKENcannot bypass.smokeproves the built artifact works, in a cleanpython:3.12-slimcontainer with no repo sources on the path. It installs the downloaded wheel ([duckdb]) into a fresh venv, assertsdlt_ops.__version__equals the released version, runsdlt-ops --help, then copiestests/andexamples/out of the checkout and runs the E2E suite against the installed package. One E2E step is deselected — it drives the console script throughuv runagainst the repo checkout, which would exercise repo sources rather than the wheel; the console script is proven directly by the--helpstep instead.publish-pypiuploadsdist/to PyPI via trusted publishing: OIDC (id-token: write), no API token, running in thepypiGitHub environment. The publish action emits PEP 740 attestations by default. It runs only after smoke passes.summaryalways runs, writing the version, GitHub Release URL, and PyPI result — or "no release — no version-bumping commits since the last tag" — to the job summary.
The changelog is machine-owned¶
CHANGELOG.md is written by PSR at release time (changelog.mode = "update"): new version sections are inserted below the <!-- version list --> marker from the commit history. Do not hand-edit it. PSR's own chore(release): commits are excluded from it (exclude_commit_patterns), and your PR title is the changelog entry.
Where next¶
- Contributing — why PR titles are conventional commits, the pipeline's input
- Versioning — the 0.x contract and the 1.0 criteria
- Compatibility — the dlt floor that ships with each release