Reusable workflows and composite Actions shared by the tox-dev repositories. They run tox and CodeQL, check pull request news, rehearse and prepare releases, push App-authored release commits and tags, publish GitHub Releases, JetBrains plugins, pre-commit mirrors and LaTeX PDFs, and keep dependencies current. Each repository keeps its own dispatch form, publisher, GitHub environments, and required check names; the shared pieces hold the steps every repository repeats and the checks that guard its keys.
Two rules run through all of them:
- Keys stay away from project code. Jobs that run hooks, tests, or builds hold read-only tokens. A writer job gets
the App key or
contents: write, checks the patch or artifact it was handed, and refusespull_request,pull_request_target,workflow_run,issue_comment, andmerge_grouptriggers. Writer jobs run no project code, except the pre-commit mirror, which runs the repository's hooks and refuses those triggers for that reason. - Tools arrive cooled. Release tools install hash-checked from the caller's
pylock.toml; tools the Actions need for themselves install from PyPI only once a release is seven days old, by default. See Locked tools and cutoffs.
Reusable workflows, called with uses: tox-dev/github-workflows/.github/workflows/<file>@SHA. The caller job must grant
at least the listed permissions; "App key" means it also passes the RELEASE_APP_PRIVATE_KEY secret.
| Workflow | Purpose | Caller grants | Docs |
|---|---|---|---|
check.yaml |
Run tox environments, one job per row, with a gate. | contents: read |
PR checks |
cooled-hooks.yaml |
Run every prek hook over the tree, environments built under the cutoff. | contents: read |
PR checks |
prepare-towncrier-release.yaml |
Build the Towncrier changelog, push the release commit and tag; or dry run. | contents: read, pull-requests: read; App key |
Prepare |
prepare-release-creator.yaml |
Same for multi-package or Rust repositories with a caller-assembled changelog. | contents: read, pull-requests: read; App key |
Prepare |
validate-python-release.yaml |
Build a wheel and sdist from the tag against locked build constraints. | contents: read |
Publish |
build-maturin-wheels.yaml |
Build maturin wheels for a matrix, plus the sdist. | contents: read |
Publish |
release-notes.yaml |
Render the GitHub Release body from the changelog. | contents: read |
Publish |
github-release.yaml |
Verify the PyPI files, then publish the GitHub Release. | contents: write |
Publish |
upgrade-release-tools.yaml |
Refresh pylock.toml and open a pull request. |
contents: read; App key |
Maintenance |
update-python-dependencies.yaml |
Bump dependency floors, relock, and open a pull request. | contents: read; App key |
Maintenance |
open-maintenance-pr.yaml |
Turn a patch from a read-only job into one pull request. | contents: read; App key |
Maintenance |
codeql.yaml |
CodeQL analysis, one job per language. | contents: read, security-events: write |
Security |
scorecard.yaml |
OpenSSF Scorecard analysis, published and uploaded to code scanning. | eight scopes, listed on its page | Security |
jetbrains-plugin-check.yaml |
Build, verify, and UI-test a JetBrains plugin. | contents: read |
JetBrains |
publish-pre-commit-mirror.yaml |
Mirror a Python package's hook as a pre-commit repository. | contents: write |
pre-commit mirror |
latex-to-pages.yaml |
Compile a LaTeX document and deploy the PDF to GitHub Pages. | contents: write, pages: read |
LaTeX |
Composite Actions, called with uses: tox-dev/github-workflows/<name>@SHA:
| Action | Purpose | Docs |
|---|---|---|
required-checks |
Gate job: fail unless every needed job succeeded. | PR checks |
validate-news |
Require a news fragment on human PRs; check fragments and lock changes. | PR checks |
build-python-distribution |
Keyless wheel and sdist build for PR jobs. | PR checks |
install-locked-tools |
Install selected groups from the caller's pylock.toml. |
Locked tools |
export-build-constraints |
Write hashed build constraints from a lock. | Locked tools |
refresh-release-lock |
Refresh or check the Nab lock. | Locked tools |
check-nab-exceptions |
Fail on an undated or expired cutoff exception. | Locked tools |
cooled-prek |
Run prek with hook environments under the seven-day cutoff. | Locked tools |
read-towncrier-config |
Read changelog path, fragment directory, and format from HEAD. |
Prepare |
prepare-towncrier |
Select the version, build and format the changelog, write a patch. | Prepare |
validate-release-patch |
Apply a release patch and check its paths and modes. | Prepare |
commit-and-tag |
Commit as the App bot and push main and the tag atomically. |
Prepare |
render-release-notes |
Render the Release body to a file. | Publish |
select-previous-tag |
Find the preceding release tag. | Publish |
reconcile-pypi-artifacts |
Compare built files with PyPI and verify their attestations. | Publish |
publish-github-release |
Create, resume, or verify a GitHub Release with its assets. | Publish |
sign-jetbrains-plugin |
Sign a plugin ZIP in a no-checkout job. | JetBrains |
stage-jetbrains-marketplace |
Check a signed ZIP against the Marketplace before and after upload. | JetBrains |
publish-jetbrains-marketplace |
Upload a signed ZIP once per tag, with a receipt. | JetBrains |
prepare-jetbrains-post-release |
Prepare the next -dev version commit without a token. |
JetBrains |
publish-post-release-pr |
Push a prepared commit and open its pull request. | JetBrains |
A typical Python library wires the pieces up with the caller files below. Read Release flow for how
they connect and the one-time setup: a repository-only release App, the release-auth and release environments, and
the lock groups.
.github/workflows/check.yaml runs the tests and the release dry run, and holds the required gate:
name: 🧪 check
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: ["main"]
tags-ignore: ["**"]
permissions: {}
concurrency:
group: check-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: 🧪 Test
uses: tox-dev/github-workflows/.github/workflows/check.yaml@SHA # 6.6.0
permissions:
contents: read
with:
include: |
[{"env": "3.15"}, {"env": "3.14", "python": "3.14"}, {"env": "type"}, {"env": "pkg_meta"}]
release:
name: 🏷️ Release dry run
uses: ./.github/workflows/prepare-release.yaml # zizmor: ignore[self-repository] actionlint rejects the $/ form
permissions:
contents: read
pull-requests: read
with:
dry-run: true
gate:
name: 🚦 Required checks pass
if: ${{ !cancelled() }}
needs: [test, release]
runs-on: ubuntu-26.04
permissions: {}
steps:
- uses: tox-dev/github-workflows/required-checks@SHA # 6.6.0
with:
needs: ${{ toJSON(needs) }}The release job references the caller's own workflow with ./ so that actionlint accepts it; a repository that does not
run actionlint can write $/.github/workflows/prepare-release.yaml instead, see
Dry run on pull requests.
.github/workflows/prepare-release.yaml is dispatched by hand to release, and called by the gate as a dry run:
name: Prepare release
on:
workflow_dispatch:
inputs:
bump:
type: choice
options: [auto, major, minor, patch]
default: auto
workflow_call:
inputs:
dry-run:
type: boolean
required: true
permissions: {}
jobs:
prepare:
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@SHA # 6.6.0
permissions:
contents: read
pull-requests: read
with:
bump: ${{ inputs.bump || 'auto' }}
dry-run: ${{ inputs.dry-run == true }}
secrets:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}The remaining files, each shown in full on its page:
| Caller file | Calls | Example |
|---|---|---|
news.yaml (on labeled/unlabeled too) |
install-locked-tools, validate-news |
PR checks |
release.yaml (on tag push) |
validate-python-release, release-notes, PyPI, github-release |
Publish |
update-dependencies.yaml (weekly) |
update-python-dependencies or upgrade-release-tools |
Maintenance |
codeql.yaml, scorecard.yaml (weekly, push) |
codeql, scorecard |
Security |
Require two checks in the main ruleset: the gate (🚦 Required checks pass) and the news job.
Releases are tagged X.Y.Z with no v prefix and no moving major tag such as v6. Each is listed in the
changelog. Pin every workflow and Action from this repository to the full commit SHA of a release tag,
with the version in a trailing comment:
uses: tox-dev/github-workflows/.github/workflows/check.yaml@2a3b863f4563ce9891d2cee1ed381b6f82654836 # 6.6.0A full SHA is the only reference GitHub cannot move, as its
security guidance for third-party actions
explains. SHA in the examples means that commit. Pin every piece to the same release: a reusable workflow runs the
Actions from its own commit, so mixed pins mix versions.
Before moving a pin across releases, read each "Breaking changes" section you skip: 6.0.0, 5.0.0, 4.0.0, 3.0.1, 3.0.0, 2.0.0, and 1.0.0. Most breaking changes ship in a major release, but 3.0.1 shows that a patch release can carry one too.
The shared pieces leave these to each caller on purpose:
- PyPI upload. Trusted publishing does not work from a reusable workflow, so the upload job lives in the caller's tag workflow.
- Project-specific build steps, such as PGO, README generation, or vendoring, beyond the documented pre- and post-build commands.
- Required check names, triggers, and schedules. Callers name their gate and schedule their own runs.
- Cutoff exceptions. The caller adds and removes
[tool.nab]exceptions; the shared checks only enforce their expiry dates.
- Release flow: how the pieces connect, the diagram, and the one-time repository setup.
- Prepare and tag a release and Publish a release.
- Pull request checks: tox, the gate, news fragments, and the release dry run.
- Maintenance pull requests: lock refresh, dependency bumps, generated changes.
- Locked tools and cutoffs: the caller's lock, cutoffs, exceptions, and prek.
- Security scanning, JetBrains plugins, pre-commit mirror, and LaTeX to Pages.
- Troubleshooting: error messages from the job logs and what to do about each.
See Contributing for local checks and news fragments.