- Overview
- Repository layout
- Dependency management
- CI workflows
- Test coverage
- Python version support
- Releases
- conda-forge packages
- The tskit.dev website
This document describes the standard layout, tooling, and CI conventions for core tskit-dev Python packages.
Core principles:
- Consistency. All repos follow the same layout and conventions, making it easy to work across the ecosystem and reducing the maintenance burden of each individual repo.
- Centralised CI logic. Shared workflows and composite actions in this repo
(
tskit-dev/.github) avoid duplication. Individual repos call shared workflows via a pinned version tag, e.g.uses: tskit-dev/.github/.github/workflows/<name>.yml@v15(see CI workflows for the versioning policy). - Reproducibility. All dependencies are managed exclusively with
uvand locked via committeduv.lockfiles. Lint tools are pinned to exact versions so results are identical locally and in CI. - Minimal dependencies. Dependencies should be declared in the appropriate group and pinned only where necessary (lint tools being the main exception).
There are three categories of package in this ecosystem:
- Python-only: tszip, tstrait, tsdate, tsbrowse, pyslim, tscompare, sc2ts
- Python+C (external C library): tskit, kastore — the C library has a public API, is independently versioned, and is consumed by other projects
- Python+C (internal C library): msprime, tsinfer — the C code is bundled as part of the Python package and is not released separately
The rules differ slightly between these categories, as described below.
pyproject.toml and uv.lock live at the root. Tests live in tests/.
prek.toml lives at the root.
The Python package and its pyproject.toml and uv.lock live in a python/
subdirectory. The standalone C library lives in c/.
prek.toml lives at the root.
Documentation (docs/) lives at the repository root and includes a doxygen/
subdirectory for generating C API docs.
The Python package and its pyproject.toml and uv.lock live in the
root, along with prek.toml. The C extension code lives in lib/.
There is no separate C API documentation or independent C release workflow.
All shared workflows that accept a pyproject-directory input should be passed
python for these repos.
We use uv exclusively for Python package management.
Every repo must define these dependency groups in pyproject.toml:
- test – pytest and test-only dependencies
- docs – JupyterBook and Sphinx extensions
- lint – linting tools (see below)
- packaging –
twineandvalidate-pyproject[all]
Python+C repos additionally require:
- wheels –
cibuildwheel
A dev group that includes all groups except wheels is recommended for
local development convenience.
Lint tools must be pinned to exact versions in the lint group to ensure
deterministic results:
[dependency-groups]
lint = [
"clang-format==x.y.z", # Python+C repos only
"ruff==x.y.z",
"prek==x.y.z",
]Ruff can be updated periodically, with any required new fixes or "ignores" included.
A uv.lock file must be maintained and committed to the repository. Run
uv lock after any change to dependencies. For Python+C repos the lock file
lives alongside pyproject.toml in python/.
Lock files should be updated periodically (say every 6 months) as part of routine maintenance. This provides the opportunity to detect and fix problems caused by upstream packages in a controlled fashion.
Each repo calls shared workflows defined in this repository. Shared workflows are
versioned via git tags on this repository (v5, v6, … v14, etc.). Calling repos
pin to a specific tag, e.g.:
uses: tskit-dev/.github/.github/workflows/python-tests.yml@v15When a change is made to a shared workflow or composite action in this repo, create a
new tag (incrementing the version number) to publish the change. While developing
the changes you can use @main instead of the tagged version to pick up the
latest changes. Calling repos must
then update their pin to pick it up — this is a deliberate opt-in. To update a repo,
find all occurrences of the old tag in its .github/workflows/ files and bump them to
the new version.
The available workflows are:
| Workflow | Purpose |
|---|---|
docs.yml |
Build and deploy documentation |
python-tests.yml |
Run the pytest suite with coverage |
lint.yml |
Run prek linting |
python-packaging.yml |
Validate packaging (sdist, wheel, twine) |
build-wheels.yml |
Build binary wheels (Python+C only) |
python-c-tests.yml |
Low-level C extension tests (Python+C only) |
c-tests.yml |
C unit tests with valgrind (Python+C only) |
Documentation is built with JupyterBook. Each repo has a docs/ directory
containing a Makefile, build.sh, and JupyterBook configuration.
Local build:
cd docs && make
This calls build.sh, which runs JupyterBook with verbose output and error
reporting. For Python+C repos, the Makefile also runs Doxygen before the build
(only when C headers have changed, so repeated builds are fast). Clean build
output with make clean.
CI build:
The docs.yml shared workflow calls the build-docs composite action
(.github/actions/build-docs), which:
- Installs the
docsdependency group - Substitutes the
__PKG_VERSION__placeholder indocs/_config.ymlwith the installed package version - Optionally runs a pre-build command (e.g. for Doxygen)
- Runs JupyterBook
On merge to main, the workflow triggers a rebuild of the tskit.dev website.
Two documentation versions are maintained: latest (built from main) and
stable (built from the most recent release tag).
Requirements:
- A
docsdependency group inpyproject.toml - A
__PKG_VERSION__placeholder string indocs/_config.yml
For Python+C repos, pass Doxygen setup as inputs to the shared workflow:
with:
additional-apt-packages: doxygen
pre-build-command: cd docs/doxygen && doxygenThe python-tests.yml workflow runs pytest with branch coverage and parallel
execution, uploading results to CodeCov with the python-tests flag. Tests
should cover a minimum and a recent Python version across Linux, macOS, and
Windows.
Some repos have their own version of the tests.yml and don't use the shared workflow because bespoke actions need to be taken or difficult system libraries need to be installed.
The lint.yml workflow installs only the lint group (no system dependencies
or package installation required) and runs prek against all files. The
prek.toml at the repo root defines all rules using only builtin and local
hooks for long-term stability.
Standard hooks cover: large file detection, merge conflict markers, line
endings, YAML/TOML validation, ruff check, and ruff format. Python+C repos
additionally run clang-format on C files.
Install prek as a pre-commit hook with uv run prek install. If local results
differ from CI, run uv run prek cache clean.
The python-packaging.yml workflow validates pyproject.toml, builds sdist
and wheel artifacts, and checks them with twine --strict. Pass a
cli-test-cmd input (e.g. tszip --help) for packages with entry points to
verify the entry point is correctly declared.
python-c-tests.yml builds the C extension with coverage instrumentation
and runs focused low-level interface tests, uploading results to CodeCov with
the c-python flag.
c-tests.yml runs the C unit tests under gcc (with coverage), clang, and
valgrind. Coverage is uploaded with the C flag.
Each repo needs the following configured before releases, coverage uploads and website rebuild triggers to work:
CODECOV_TOKENGitHub Actions secret — for coverage uploads (see Test coverage).ADMINBOT_TOKENGitHub Actions secret — used by the shareddocs.ymlworkflow to fire arepository_dispatchto tskit-site after a successful docs build onmain. Without this, merging documentation changes will not trigger a site rebuild.releaseGitHub Actions environment — required bywheels.ymlfor the Trusted Publisher OIDC token (id-token: write). Create this environment in the repo's Settings → Environments.- Trusted Publisher on PyPI and TestPyPI — configure a Trusted Publisher entry on
both pypi.org and test.pypi.org pointing
to the repo and the
releaseenvironment. Without this, thewheels.ymlupload steps will fail even if the workflow runs successfully.
Coverage is monitored by CodeCov. The CODECOV_TOKEN secret must be set in
each repo's GitHub Actions secrets. Verify uploads are working by checking the
Actions logs after a CI run.
Flags used: python-tests, c-python (Python+C only), C (Python+C only).
Each repo should include a codecov.yml at the repository root. This file
controls how CodeCov interprets and reports coverage data.
When adding or removing support for a Python version, update the following in every affected repo:
pyproject.toml (or python/pyproject.toml for Python+C repos):
requires-python— set the floor version, e.g.">=3.12".classifiers— add or remove the corresponding"Programming Language :: Python :: 3.X"entry. This is informational (displayed on PyPI) but should be kept in sync withrequires-pythonand the test matrix.[tool.ruff] target-version— must match the minimum supported version (e.g."py312"). Forgetting this means ruff silently permits syntax that is invalid on the stated floor version.
.github/workflows/tests.yml:
- Update the
matrix.pythonlist. Convention is to test the oldest and newest supported versions; intermediate versions do not need individual matrix entries.
[tool.cibuildwheel] in pyproject.toml:
- Update the
buildlist, e.g.["cp311-*", "cp312-*", "cp313-*"]. This is what controls which wheels are built — it is not inferred from the classifiers. - When adding a new Python version, check that it is supported by the pinned
cibuildwheelrelease in thewheelsdependency group. Pre-release Python versions may require a newer cibuildwheel or explicit opt-in. - After updating, push to the
test-publishbranch and verify that all new wheels build and pass their smoke tests before making a release. Wheel builds are not otherwise exercised in CI between releases, so this step is important.
Check all .github/workflows/ files in the repo for any explicit references to the
old minimum Python version and update them to the new minimum. In particular:
- Any bespoke workflow steps that install or invoke a specific Python version directly.
Python releases use the wheels.yml workflow via the Trusted Publisher PyPI mechanism.
Because Trusted Publisher requires the upload to originate from the source repository,
this workflow is not shared — each repo maintains its own copy.
Python+C repos additionally use the shared build-wheels.yml workflow, which uses
cibuildwheel to build binary wheels across Linux, macOS, and Windows. All
configuration lives in pyproject.toml under [tool.cibuildwheel].
tskit and kastore also have a repo-specific release-c.yml workflow solely for C API
releases (see below). This is separate from the Python release process.
Most repos use setuptools_scm for versioning, which means that versions are automatically
created from git tags. This approach doesn't work for kastore and tskit, though, and so they
maintain version numbers manually.
tskit and kastore: follow the tskit and kastore releases section below instead of this standard process.
- Prepare a PR that updates the CHANGELOG with the correct version number. Merge this PR.
- Push to the
test-publishbranch on upstream to triggerwheels.yml, which builds release artifacts and publishes them to TestPyPI. Check the "Publish Python release" action succeeds. This step is especially important for repos with binary wheels (msprime, tsinfer, tskit, kastore) as the wheel-building step is not tested between releases. To find the results of the build action, go to the "Actions" tab in github, and look for the action associated with this branch; next go tohttps://test.pypi.org/project/<package>/#historyto see if the upload worked. - Once the TestPyPI upload succeeds, delete the
test-publishbranch. Go to the "Releases" section on GitHub and click "Draft new release" (see the names of previous releases for naming convention). Enter the version number in the tag box and click "create new tag" (the tag is created when the release is published). Fill in the release body with the CHANGELOG contents (you can copy-paste from the CHANGELOG in the "latest" docs on tskit.dev to get the formatting right). Click "Publish release" and confirm the "Publish Python release" action succeeds (under the Actions tab in github) and that it is successfully available on PyPI. - Open a post-release PR that opens a new section in the CHANGELOG.
tskit and kastore each have an independently versioned C library and a Python package,
and do not use setuptools_scm. The steps below apply to both repos.
release-c.yml handles C API releases only — it is triggered by tags containing C_
and has no effect on the Python package.
- Prepare a PR that:
- Updates the version macros in the C header and
c/VERSION.txt - Updates
c/CHANGELOG.rstwith the release date and version; check completeness by comparinggit log --follow --oneline -- cwithgit log --follow --oneline -- c/CHANGELOG.rst
- Updates the version macros in the C header and
- Merge the PR.
- Tag and push:
git fetch upstream git checkout upstream/main git tag -a C_MAJOR.MINOR.PATCH -m "C API version C_MAJOR.MINOR.PATCH" git push upstream C_MAJOR.MINOR.PATCH - After a couple of minutes,
release-c.ymlwill build the release tarball and create a draft release on the repo's GitHub releases page. Update the release body with the changelog contents and publish. - Open a post-release PR that opens a new section in
c/CHANGELOG.rstand closes the GitHub issue milestone.
Follow the standard Python release process above, with two differences:
- In step 1, also set the version in
python/<packagename>/_version.py(these repos do not usesetuptools_scm). - In step 4, also bump the version in
python/<packagename>/_version.pytoMAJOR.MINOR.PATCH.dev0.
Several tskit-dev packages are distributed via conda-forge:
tskit, msprime, kastore, tstrait, tszip, and tsinfer.
Each has a feedstock repository at https://github.com/conda-forge/<package>-feedstock.
conda-forge bots monitor PyPI and automatically open a PR on the feedstock repository
when a new release is published. This PR updates the version number and source hash in
recipe/meta.yaml. In most cases the PR can simply be merged with no manual
intervention — the bot handles the version bump and rebuilds.
Dependency changes are not picked up automatically. Whenever a package's
dependencies change (additions, removals, or version constraint changes), the
corresponding feedstock's recipe/meta.yaml must be updated manually:
run:requirements must reflect the current[project.dependencies]inpyproject.toml.
The tskit-site repository is the source for
the tskit.dev website, including the landing page and the hosted
documentation for all packages in the ecosystem. It is a Jekyll site deployed to GitHub
Pages via the gh-pages branch.
The deploy.yml workflow orchestrates the full build. It runs on:
- Every push to
main(live deployment) - Every pull request (preview artifact only — not deployed)
- A daily scheduled cron job (keeps the site up to date with any upstream changes)
- A
repository_dispatchevent (triggered automatically by per-repo doc builds — see below) - Manual
workflow_dispatch
deploy.yml calls three sub-workflows in parallel, then assembles and deploys the result:
build-core.yml— builds the Jekyll site itself (landing page, software listings, news, etc.) using Ruby/Gruntbuild-docs.yml— builds per-package documentation for bothlatest(frommain) andstable(from the most recent release tag) versionsbuild-extras.yml— imports the tutorials site (fromtskit-dev/tutorials), rust tutorials (fromtskit-dev/tskit-rust), and builds the tskit-explore JupyterLite app
The assembled site is deployed to gh-pages. Package docs land at
/<package>/docs/latest and /<package>/docs/stable; a redirect page at
/<package>/docs/index.html points visitors to the stable version by default.
Each repo's own docs.yml uses the shared docs.yml workflow defined in this
repository. On merge to main, after a successful build, the shared workflow fires a
repository_dispatch event to tskit-site, which triggers a full site rebuild. This
means that merging a documentation change to any repo automatically propagates to
tskit.dev within a few minutes.
The build-docs.yml in tskit-site uses the same shared build-docs composite action
(.github/actions/build-docs) that the per-repo workflow uses, and passes the same
inputs (pyproject-directory, additional-apt-packages, pre-build-command). Builds
are cached by commit SHA, so a package's docs are only rebuilt when its main branch
has a new commit.
When making changes that affect how docs are built (e.g. new APT dependencies, a new
pre-build command, changes to build.sh or the docs dependency group), be aware that
the build runs in two places:
- The repo's own
docs.yml— configured via inputs to the shareddocs.ymlworkflow in the repo's.github/workflows/docs.yml. - tskit-site's
build-docs.yml— has a matrix entry per package that passes the same inputs. This file must be updated in thetskit-dev/tskit-siterepo to match.
If these two are out of sync, the per-repo CI will pass but the tskit-site build will
fail or produce incorrect output. The relevant matrix entry in build-docs.yml is
identified by the package name and specifies additional-apt-packages,
pre-build-command, pyproject-directory, and a cache-version (bump this whenever
the build environment changes to force a cache miss).
Changes to build.sh, docs/_config.yml, or the docs dependency group are picked up
automatically on the next run with no changes needed to tskit-site.
latest is built from main on every new commit. stable is built from the most
recent release tag, determined by sorting all tags and selecting the newest that does
not contain b, a, or C_ in its name (i.e. no pre-releases or C API tags).
The critical distinction: stable docs are built from the repo code at the tag, but
using the current shared build infrastructure — the pinned build-docs action version
and whatever additional-apt-packages and pre-build-command are currently in the
tskit-site matrix entry for that package. The stable build does not use main's
build.sh or docs/ content; it uses whatever was committed at the tag.
This means:
-
Content and structural changes (new pages, edited text, updated notebooks) only affect
latestuntil a new release is tagged.stablecontinues to serve the old content. This is expected behaviour. -
Build environment changes (new APT packages, changed
pre-build-command) require updating the tskit-site matrix entry, which applies to bothlatestandstable. If the stable release's code is not compatible with the updated build environment, the stable build will break. -
Bumping
cache-versionin the tskit-site matrix entry forces a cache miss for both versions. This is required when the build environment changes, but it also means the stable docs will be fully rebuilt — so the same compatibility concern applies.
When making a change that alters the build environment, verify that the stable version
still builds correctly, or make a new release first so that stable and latest point
to the same code.