Releasing
This page is the operator's runbook for the branch model, the CI gate that protects it, and the tag-driven release flow that publishes taxi-seed to PyPI. If you are looking for how to build and test locally as a contributor, see Contributing instead — this page is about promoting code from a merged PR to a published package.
Branch model
devis the default branch and the integration branch. Feature branches are cut fromdev, and pull requests targetdev— notmain.mainis the stable, released branch. Nothing lands onmaindirectly; it only moves forward by mergingdevinto it oncedevis in a state worth shipping.- The docs site deploys from
main(see thedocsjob in.github/workflows/ci.yml, gated onpushtorefs/heads/main), somainis also "what's live on the docs site" at any given moment. - Releases are tagged on
main, after the promotion merge — never ondevand never on a feature branch. The version published to PyPI is whatevermainlooked like when the tag was pushed.
The promotion path in order: feature branch → PR into dev → dev passes CI → PR/merge dev into main → tag main → release workflow runs.
The CI gate
Every pull request runs two jobs defined in .github/workflows/ci.yml:
test— unit tests across the Python version matrix (3.12, 3.13).integration— end-to-end tests against a real SQL Server container (loader integration + pipeline e2e), on Python 3.13.
Merging into dev or main is blocked until integration passes — it is configured as a required status check (see the one-time setup below for the exact command that wires this up).
The required check is named integration
Branch protection matches on the job name, not on what the job does. If the integration job in .github/workflows/ci.yml is ever renamed, the branch-protection rule silently stops matching anything — GitHub will show the merge button as available even though no equivalent check ran. Renaming that job and updating branch protection have to happen in the same PR.
Releasing (test → prod)
The published version is derived entirely from the git tag via hatch-vcs — there is no version string to bump in pyproject.toml. Pushing a tag is the release action.
.github/workflows/release.yml triggers on any tag matching v* and classifies it as prerelease or final by shape:
| Tag shape | Example | Classified as | Destination |
|---|---|---|---|
vX.Y.Z |
v0.2.0 |
final | PyPI + GitHub Release |
anything else matching v* |
v0.2.0rc1, v0.2.0a1, v0.2.0b1, v0.2.0.dev1 |
prerelease | TestPyPI + GitHub prerelease |
Both paths run the same build job first (checkout with full history/tags for hatch-vcs, uv sync --extra test, pytest -q as a guard, uv build), then fan out to testpypi or pypi based on the classification, then create the GitHub release/prerelease from the built artifacts. Publishing uses OIDC Trusted Publishing (id-token: write) — there are no PyPI tokens stored anywhere in the repo or its secrets.
Cut a prerelease (TestPyPI)
From an up-to-date main:
This runs the build job, then the testpypi job (environment testpypi, no approval required), then creates a GitHub prerelease. Watch it in the Actions tab or with:
Verify the published artifact installs before promoting further:
Cut a final release (PyPI)
Once the prerelease has been verified, tag the same commit (or a later one on main) with the final version:
This runs build, then the pypi job — which waits for approval in the pypi GitHub Environment (the required reviewer configured during one-time setup) before it publishes — then creates the GitHub Release. Approve the deployment from the Actions run page (or gh run watch, which surfaces the pending-approval state) when you're ready for it to go live.
Safety / gotchas
- The required reviewer on the
pypiEnvironment is the only gate protecting production PyPI. If that environment is created without a required reviewer (or not created at all), a finalvX.Y.Ztag publishes to real PyPI unattended — there is no other approval step in the path. Don't skip step 3 of the one-time setup below. release.ymltriggers on anyv*tag pushed to any branch, not justmain. Only create release tags onmain, per the branch model above — an accidentalv1.0.0tag pushed on a feature branch or ondevwould build from that commit and publish it just the same.
One-time setup checklist
These are manual, out-of-band steps — they only need to happen once per repo, not per release.
-
Create and default
dev.Then in GitHub: Settings → General → Default branch → switch it to
dev. New PRs andgit clonewill targetdevfrom then on. -
Register PyPI + TestPyPI Trusted Publishers. Both PyPI and TestPyPI support registering a pending publisher before the project has ever been uploaded — this is what lets the very first
pypi/testpypijob authenticate via OIDC with nothing to configure on the GitHub side beyond the environment name. On each of pypi.org and test.pypi.org, add a pending publisher with:Field Value PyPI project name taxi-seedOwner andrekammanRepository name taxi-seedWorkflow name release.ymlEnvironment name pypi(on pypi.org) /testpypi(on test.pypi.org) -
Create the GitHub Environments. Settings → Environments:
testpypi— no protection rules. The prerelease flow should be fast and unattended.pypi— add yourself (or the release approver) as a required reviewer. This is the actual production approval gate: thepypijob inrelease.ymlwill not run until someone approves the deployment.
-
Turn on branch protection for both
devandmain, requiring theintegrationcheck on each. Both branches get the identical rule —devbecause feature-branch PRs land there, andmainbecausedev→mainpromotion PRs need the same gate to keepmainreleasable:Note: GitHub only lists a check name as selectable in the Settings UI after it has run at least once on the repo (e.g. via one open PR) — but the API above accepts the
integrationname regardless of whether the UI has seen it yet, so this can be run before or after that first PR.
Optional — custom docs domain
The docs site can be pointed at taxi-seed.com instead of the default andrekamman.github.io/taxi-seed/ URL. This is not required for anything above and can be done at any time:
-
Add a
docs/CNAMEfile to the repo containing exactly:mkdocs gh-deploycopies everything underdocs/into the published site, so this file will land at the root ofgh-pagesautomatically on the next docs deploy. 2. In GitHub: Settings → Pages, set the custom domain totaxi-seed.comand enable "Enforce HTTPS" once the certificate has provisioned. 3. Add the DNS records at your registrar: either anALIAS/ANAME(or the four GitHub PagesArecords) for the apex domain, or aCNAMErecord if serving from a subdomain — pointing atandrekamman.github.io.