Skip to content

CI workflow

This page documents .github/workflows/ci.yml.

The CI workflow is TopMark's primary source-tree validation workflow. It validates pull requests, pushes to main, and version-tag pushes before release publication consumes CI-built artifacts.

Note

The canonical vocabulary used throughout the documentation is defined in Terminology and Canonical Vocabulary.

Purpose

The CI workflow validates that the repository source tree is healthy before changes are merged or released. It checks formatting, linting, typing, documentation integrity, link integrity, test behavior, and stable public API compatibility.

The workflow also builds release artifacts on version-tag pushes. This is intentional: release artifacts are built in the unprivileged CI workflow and later consumed by the privileged release workflow, so the release workflow does not rebuild the project from repository source code.


Trigger conditions

Trigger When it runs Purpose
pull_request Pull requests affecting source, tests, tools, documentation, workflow, dependency, or validation files Validate proposed changes before merge
push.branches Pushes to main Validate committed source changes
push.tags Tags matching v* Validate the tagged source tree and build release artifacts

Pull-request runs are path-filtered at the workflow level so unrelated changes do not trigger CI. Within the workflow, the changes job performs finer-grained path filtering so jobs such as lint, tests, docs, link checks, pre-commit validation, and API snapshot checks run only when relevant.

Version-tag pushes are not path-filtered by pull-request change groups. They run the release-artifact path after the required validation jobs succeed.


Permissions and trust boundary

The workflow uses read-only repository permissions by default:

permissions:
  contents: read

The release-artifacts job also declares contents: read explicitly. The workflow does not publish packages, create releases, or request elevated release permissions.

The trust boundary is intentional:

  • CI checks out and runs repository code in an unprivileged context.
  • CI builds release artifacts from the tagged source tree.
  • The privileged release workflow later downloads, verifies, and publishes those artifacts instead of rebuilding them.

This separation keeps artifact production and package publication in separate trust boundaries.

Some setup logic is shared through the local composite action setup-python-nox, while other jobs keep explicit setup steps where their caching or environment needs differ. This limited duplication is acceptable because it keeps job behavior and trust boundaries explicit.


Jobs and validation scope

Job Purpose Main tools
changes Detect changed file groups for PR job gating dorny/paths-filter
python-metadata Resolve supported and canonical Python versions for CI jobs nox, pyproject.toml
lint Validate formatting, linting, typing, and docstring links nox, ruff, pyright
pre-commit Run configured pre-commit hooks pre-commit
docs Build the documentation site in strict mode nox, mkdocs
tests Run the supported Python test matrix nox, pytest
coverage Generate and publish canonical coverage reports nox, coverage.py, pytest
api-snapshot Check public API stability for source-changing pull requests nox, tools/api_snapshot.py
links Validate links in source Markdown files lycheeverse/lychee-action, lychee.toml
links-site Validate links in the rendered MkDocs site, including generated pages mkdocs, lycheeverse/lychee-action
release-artifacts Build and upload release artifacts for version tags uv build, actions/upload-artifact

Most jobs delegate validation to nox sessions so local development and CI share the same stable validation contracts and execution semantics. The python-metadata job resolves supported Python versions and the canonical single-version Python from project metadata through nox -s print_python_matrix. The test matrix consumes that supported-version list with fail-fast disabled so failures on one Python version do not hide failures on others.

Coverage reporting runs in a dedicated canonical job on Ubuntu using the resolved canonical Python version and the existing nox -s coverage session. Coverage intentionally runs outside the full test matrix to avoid duplicating expensive QA work that is already covered by the compatibility matrix.

The coverage job depends on the full test matrix succeeding before coverage reports are generated. This keeps the compatibility matrix as the primary validation gate while avoiding additional coverage-processing noise after known test failures.

Canonical single-version jobs such as linting, documentation builds, coverage, API snapshot checks, and release-artifact construction use the same resolved canonical Python value rather than carrying separate hard-coded version literals in the workflow.

Note

Shared Python/bootstrap jobs intentionally use explicit actions/cache ownership while

keeping the setup-uv built-in cache integration disabled. This avoids cache-reservation race warnings between concurrent jobs using identical bootstrap inputs.

The API snapshot check is pull-request-only and runs when Python-relevant files change. It is a fast guardrail for unexpected stable public API surface changes, not a replacement for the full test matrix.

Documentation integrity is validated at multiple levels:

  • the docs job runs a strict MkDocs build;
  • the links job validates links in source Markdown files;
  • the links-site job validates links in the rendered site, including generated API pages.

Generated API pages are visible only after the site is built, so source-only link checks cannot fully replace the built-site link check.


Artifact handling

The CI workflow produces release artifacts only for tag pushes matching v*.

The workflow also publishes coverage artifacts from the dedicated coverage job:

  • an HTML coverage report;
  • XML and JSON machine-readable coverage reports;
  • a short GitHub Step Summary with a coverage overview and artifact notice.

Coverage artifacts are diagnostic CI outputs only. They are not release artifacts and are not consumed by the release workflow.

On a version-tag push, the release-artifacts job:

  • builds the source distribution and wheel with uv build;
  • derives release metadata from the tag;
  • normalizes the tag version with packaging.version.Version;
  • records the supported and canonical Python metadata used by the CI run;
  • writes checksum metadata with sha256sum;
  • uploads topmark-dist and topmark-release-meta artifacts.

The release workflow consumes those uploaded artifacts after validating the triggering CI run and tag context. CI does not publish the package itself.

This artifact handoff is part of the release trust-boundary model. Artifact creation happens where repository code is already expected to run; publication happens later in a privileged workflow that does not rebuild the project from repository source code.


CI validation model

TopMark intentionally separates:

  1. source-tree validation;
  2. supported Python-version discovery;
  3. canonical single-version validation jobs;
  4. compatibility test-matrix execution;
  5. diagnostic coverage reporting;
  6. release artifact construction;
  7. privileged artifact publication in the downstream release workflow.

This layered CI model keeps repository validation, release artifact creation, and package publication in separate trust boundaries while preserving stable local/CI validation contracts.


Local reproduction

The closest local equivalents are the nox sessions used by CI:

nox -s print_python_matrix
nox -s format_check
nox -s lint
nox -s docstring_links
nox -s docs
nox -s qa -p 3.13
nox -s coverage -p 3.13

The concrete 3.13 commands shown here reflect the current canonical Python version. That value is resolved from project metadata and is expected to move when the supported Python range moves.

Run link checks with:

nox -s links_all
nox -s links_site

Run the API snapshot check with:

nox -s api_snapshot -p 3.13

CI uses the resolved canonical Python value for this session rather than hard-coding the version in .github/workflows/ci.yml.

Build release artifacts locally with:

uv build

Local commands can reproduce most validation behavior, but they do not reproduce GitHub event context, pull-request path filtering, artifact-upload behavior, or downstream release-workflow handoff semantics.


Maintenance notes

Keep the CI workflow explicit enough that contributors can determine which validation contract failed from the job name and log output.

When editing this workflow:

  • update path filters when adding new source, docs, tooling, or workflow-maintenance files;
  • keep nox sessions as the canonical stable validation contracts where practical;
  • keep Python-version metadata sourced from pyproject.toml through nox -s print_python_matrix;
  • keep the coverage job canonical and lightweight rather than instrumenting the full compatibility matrix;
  • keep coverage reporting lightweight and diagnostic rather than turning it into a percentage-driven release gate;
  • keep release artifact building in CI unless the release trust model is deliberately redesigned;
  • avoid moving package publication into this workflow;
  • keep generated-site link validation separate from source Markdown link validation;
  • keep action pins synchronized across workflows and local composite actions;
  • keep uv cache ownership explicit and centralized rather than mixing setup-uv cache management with separate actions/cache ownership.

GitHub Actions are pinned to commit SHAs. Use the GitHub Action pin audit to detect drift between workflow files and local composite actions.