CI and Release Pipeline
This page describes the GitHub Actions workflows that validate Click, check workflow changes, build tagged distributions, and publish releases to PyPI.
The pipeline separates fast checks triggered by pull requests and branch pushes from broader scheduled checks, while release publication starts only from a pushed tag.
Sources: .github/workflows/publish.yaml:1-5, .github/workflows/nightly.yaml:1-5
Core concepts
Pull-request checks
Pull-request checks are workflows triggered by pull_request; standard tests ignore changes only under docs/** and README.md, while other checks use their own path rules.
Sources: .github/workflows/tests.yaml:2-7, .github/workflows/zizmor.yaml:2-7
Matrix testing
Matrix testing runs the tests job across supported Python implementations and operating systems, so one change is exercised in several environments rather than only on Ubuntu.
Sources: .github/workflows/tests.yaml:13-28
Scheduled validation
Scheduled validation runs the Nightly tests workflow at 0 6 * * * and also permits a manual workflow_dispatch run.
Sources: .github/workflows/nightly.yaml:1-5
Tagged publication
Tagged publication is the Publish workflow, which starts when any tag is pushed. Its build job exposes an artifact ID, and both release jobs download that artifact before publishing their outputs.
Sources: .github/workflows/publish.yaml:1-5, .github/workflows/publish.yaml:9-13, .github/workflows/publish.yaml:32-59
What runs on pull requests and branch pushes
The normal test workflow responds to pull requests and pushes to main or stable, excluding documentation-only and README-only changes; its concurrency group cancels an older run for the same pull request or ref when a newer one starts. The workflow has a tests job and a separate typing job.
The tests job checks out the repository, enables the uv cache, installs the matrix-selected Python version, and runs tox through uv with the development group. Its matrix covers Python 3.10 through 3.14, free-threaded Python 3.14t, PyPy 3.11, Windows, and macOS. The matrix sets fail-fast: false, so one failing environment does not stop the other matrix entries from reporting.
The typing job uses the Python version declared in pyproject.toml, caches .mypy_cache, and runs the typing environment through tox. This makes type checking a distinct CI result from the runtime test matrix.
The pre-commit workflow runs on every pull request and on pushes to main or stable; after preparing Python and the pre-commit cache, its main job runs pre-commit against all files and shows diffs on failure.
The zizmor workflow is narrower: it runs on pull requests that change YAML files and on main or stable pushes that change YAML files, then invokes the zizmor action with annotations enabled.
The following workflow view summarizes the ordinary validation path and its separate release entry point.
Evidence
- pull-request.github/workflows/tests.yaml:2
- branch-push.github/workflows/pre-commit.yaml:2
- standard-tests.github/workflows/tests.yaml:12
- typing-check.github/workflows/tests.yaml:42
- precommit-check.github/workflows/pre-commit.yaml:11
- workflow-analysis.github/workflows/zizmor.yaml:12
- tag-push.github/workflows/publish.yaml:2
- release-build.github/workflows/publish.yaml:9
- github-release.github/workflows/publish.yaml:32
- pypi-publish.github/workflows/publish.yaml:46
This diagram shows the event-to-job triggers and the artifact hand-off from build to both release jobs.
Sources: .github/workflows/tests.yaml:2-11, .github/workflows/tests.yaml:12-14, .github/workflows/tests.yaml:42-43, .github/workflows/tests.yaml:29-41, .github/workflows/tests.yaml:19-28, .github/workflows/tests.yaml:15-18, .github/workflows/tests.yaml:42-59, .github/workflows/pre-commit.yaml:1-9, .github/workflows/pre-commit.yaml:11-27, .github/workflows/zizmor.yaml:1-7, .github/workflows/zizmor.yaml:13-22, .github/workflows/tests.yaml:2-7, .github/workflows/publish.yaml:1-5, .github/workflows/publish.yaml:32-59
How nightly validation is split from routine CI
Nightly validation runs race-tests for both main and stable. Its matrix includes random tests across Python 3.10–3.14 and 3.14t, Windows, and macOS, plus stress tests on Python 3.14, 3.14t, and macOS. Each matrix entry checks out the selected branch and passes its configured tox environment through TOX_ENV.
The same nightly workflow runs flask-tests for both branches. It checks out pallets/flask, installs dependencies, and runs Flask’s pytest suite with the selected Click branch overlaid from Git using CLICK_REF. This provides a downstream compatibility check in addition to Click’s own tests.
A separate daily Lock inactive closed issues workflow performs repository maintenance rather than code validation: it runs at midnight, uses dessant/lock-threads, and sets 14-day inactivity thresholds for issues and pull requests; the shown configuration also grants write permission for discussions, but does not show a discussion inactivity threshold. The split therefore places standard tests, typing, formatting hooks, and relevant workflow analysis on development events, while random, stress, downstream, and maintenance work runs on schedules.
Sources: .github/workflows/nightly.yaml:11-18, .github/workflows/nightly.yaml:19-29, .github/workflows/nightly.yaml:30-43, .github/workflows/nightly.yaml:44-50, .github/workflows/nightly.yaml:51-69, .github/workflows/lock.yaml:7-25, .github/workflows/tests.yaml:2-7, .github/workflows/nightly.yaml:1-5
How a tag becomes a PyPI release
A tag push starts Publish; the workflow has no pull-request or branch-push trigger. The build job checks out the tagged source, installs the Python version from pyproject.toml, records the latest commit timestamp in SOURCE_DATE_EPOCH, runs uv build, and uploads dist/ as an artifact.
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- run: uv build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
id: upload-artifact
with:
name: dist
path: dist/
if-no-files-found: errorThis build step makes the artifact explicit and fails if dist/ contains no files.
Both publication jobs depend on build and download the exact artifact identified by the build output. create-release has contents: write permission and creates a draft GitHub release named from GITHUB_REF_NAME, attaching dist/*. publish-pypi uses the publish environment, grants id-token: write, and targets the Click version page on PyPI before invoking the PyPI publishing action.
Sources: .github/workflows/publish.yaml:1-5, .github/workflows/publish.yaml:9-31, .github/workflows/publish.yaml:24-31, .github/workflows/publish.yaml:32-38, .github/workflows/publish.yaml:34-45, .github/workflows/publish.yaml:46-59
How it connects
CI is the repository-level gate around the source and test suite: it invokes tox, typing, pre-commit, and zizmor, while scheduled checks additionally exercise Flask against a Click branch. For how the repository is organized and how contributors prepare a local environment, see Repository Layout and Dev Setup. For how the test suite itself is structured, see Test Suite Organization and Patterns. Release artifacts then hand off to GitHub Releases and PyPI rather than to Click runtime code.
Sources: .github/workflows/zizmor.yaml:13-22, .github/workflows/nightly.yaml:51-69, .github/workflows/tests.yaml:29-41, .github/workflows/publish.yaml:32-59
Key takeaways
- Pull requests run the standard test matrix, typing, pre-commit checks, and YAML security analysis when applicable.
- Pushes to
mainandstablerun the branch-triggered checks; documentation-only changes are excluded from the standard tests workflow. - Nightly runs add random, stress, and Flask downstream tests, while issue-locking is separate maintenance.
- A pushed tag builds one
dist/artifact, then reuses it for a draft GitHub release and PyPI publication.