Repository navigation
✨ feat(workflows): shared CI, release and update workflows - #46
Merged
Merged
Conversation
Every gate repeated a jq expression over the needs context. The new action fails unless each needed job succeeded, and it accepts named jobs that may be skipped, so a gate names only what it requires. gw's own CI moves from check.yaml to ci.yaml so the name is free for the reusable tox check workflow. Job names do not change, so required check names stay as they are.
Library repositories repeat one tox matrix: install tox, build each environment with --notest, run it with --skip-pkg-install, and gate on the results. The reusable workflow takes the matrix as JSON rows and keeps the install under the same upload cutoff the Actions use, while tox environments keep fresh dependencies. Coverage data is an opt-in artifact per job, so a caller that measures coverage across runners still merges it in its own job.
CodeQL is the same analysis in each repository and differs only in the languages it covers, the paths it skips, and its query suite. The reusable workflow takes those as inputs, and each caller keeps its own triggers and schedule. The workflow pins CodeQL Action v4.39.1. gw's own analysis still ran v4.39.0, so it moves to the same release through a thin caller.
`release-notes.yaml` renders the GitHub Release body in a read-only job and uploads it as an artifact. Its changelog defaults to the path in the committed Towncrier configuration, and the previous tag comes from `render-release-notes`, so callers drop their own previous-tag and upload steps. `render-release-notes` gains a `package` input that renders one package section of a shared Markdown changelog, for repositories that release several packages from one tag namespace.
`github-release.yaml` checks the PyPI distributions with `reconcile-pypi-artifacts`, then publishes the GitHub Release with `publish-github-release`. The job runs in the caller's `release` environment with `contents: write`, so a caller cannot forget the environment, which several callers did. The caller keeps its PyPI upload job, because trusted publishing cannot run in a reusable workflow. Callers pass `title-prefix` for tags such as `v1.2.3`, and `assets-directory` to attach the distributions. The README gains the caller examples for both workflows.
A `[tool.nab]` cutoff exception outlives its release unless someone
removes it, so the lock check now requires an expiry date for each one.
Nab rejects unknown keys in a package table, so the date sits in a
comment directly above the table header or dotted key:
# expires 2026-11-09
[tool.nab.packages.release-creator]
uploaded-prior-to = 2026-11-02T21:45:00Z
`refresh-release-lock` in `check` mode runs `check_nab_exceptions.py`
before it installs Nab. It fails, and names the package, when an
exception has no comment, a malformed date, a date already past, or a
`[[tool.nab.package-rules]]` cutoff, which the check does not read.
A standalone `nab.toml` is checked too. Dates are inclusive: the
exception is valid through its date.
The expiry check was a step inside `refresh-release-lock`, reachable only through its check mode. Tools run through actions in this repository, so the check moves into `check-nab-exceptions`, a composite Action that callers and the lock actions can call. `refresh-release-lock` calls it in check mode.
The weekly lock refresh now runs the cutoff exception check before it refreshes the lock. A caller whose `[tool.nab]` exception has passed its date, or has no date, fails the scheduled run and sees the package name, even when no release input changed. The check-mode call in `refresh-release-lock` stays. The README describes the three places the check runs.
Callers that generate a maintenance change in a read-only job can now open or update its pull request without peter-evans/create-pull-request. The read-only validate job applies the patch to a scratch index and accepts only paths that match allowed-paths as regular non-executable files or deletions. The App writer job, in the release-auth environment, checks the patch digest and paths again, and mints the App token only after both checks pass. It force-pushes the branch with a lease and edits or opens the pull request.
Add a reusable workflow that bumps the direct requirements of the files a caller names, under a seven-day upload cooldown. A run can expedite named packages past the cooldown, and cooldown 0 days turns it off. The lock is refreshed with Nab, the repository's prek hooks run twice to confirm the result is stable, and the change goes through open-maintenance-pr. The bump-deps-index bootstrap always waits out the seven-day cooldown, since tooling is cooled. kind library is refused until bump-deps-index can apply a cutoff to one dependency section; it cannot yet, so libraries cannot use this workflow.
Describe the step mapping with a TypedDict instead of Any, and read the workflow-level env so a step test sees the same variables as the runner.
kind library keeps the groups in fresh-groups (default "project test") at their newest release by passing --exclude-newer-group GROUP=false, and cools every other group. kind app passes no group. Other groups, the build backend, and hooks stay cooled. The bump-deps-index install takes the new exclude-newer and exclude-newer-package inputs, seven days by default, instead of a bootstrap cutoff hard-coded in the workflow. The bump-deps-index requirement is one BUMP_DEPS_INDEX constant in the workflow env, set to the release that adds the group and package cutoffs.
The maintenance writer pushes a branch and opens a pull request from a patch that a read-only job of the caller produced. Record that threat, its control in open-maintenance-pr, and the verdict for a caller whose allowlist admits workflow files. Update the untrusted-input row, which named only the release writer's paths.
build-maturin-wheels.yaml builds a maturin extension for a JSON build matrix. A source job runs an optional pre-build command once, each matrix entry builds in its own job, and an sdist job builds from the hash-checked build group. Artifacts keep the names callers already publish, and no job requests a write token.
validate-release-patch takes allowed-paths, glob patterns for the only paths a release patch may change. When it is set, the Towncrier configuration is not read and the changelog and fragment rule does not apply. Executable and symlink modes still fail.
prepare-release-creator.yaml follows prepare-towncrier-release: the assemble job selects the tag, links fragments, runs the caller's changelog command, and uploads a patch without a token; the push job checks the patch against release-paths and pushes the commit and tag with the release App. Repositories with several packages scope tags and fragments with package and tag-format.
Add a reusable workflow that builds the unsigned plugin ZIP and records its SHA256, runs verifyPlugin for each entry of a matrix input, runs the Xvfb UI tests with a configurable start timeout, and gates on all of them. Release callers pass the tag as plugin-version and sign the recorded ZIP. The build selects exactly one archive matching the version, and keeps the Gradle cache off because the ZIP feeds a signed release. UI-test reports upload on failure.
Add a composite that checks a signed ZIP against the Marketplace with release-creator before upload (mode stage) and against the public version afterwards (mode verify). The channel is derived from the tag suffix, so callers pass it to the publish action instead of computing it.
Add a composite that commits the next -dev plugin version without an App token. It requires a plain X.Y.Z tag and writes the manifest that publish-post-release-pr consumes, so the App token stays in the writer job.
Add a reusable workflow for pyproject-fmt and tox-toml-fmt style mirrors. - Pin pre-commit-mirror-maker and prek, and setup-uv to an exact version. - Run prek twice: exit 1 means files changed, the second run must pass. Other statuses fail the job instead of being masked. - Refuse to move a published tag. The push creates the branch and tag in one atomic push, with no force and no token in the remote URL. - Serialize runs per repository with cancel-in-progress disabled.
Add a reusable workflow that compiles a LaTeX root file with SOURCE_DATE_EPOCH pinned to the commit time, uploads the PDF, deploys it to gh-pages from main, and waits until Pages serves the built SHA256. Build and deploy jobs have timeouts, and the workflow grants no permissions at top level.
The daily mirror run failed whenever the package version had already been tagged, even when the regenerated content matched the tag. Compare the regenerated tree with the published tag's tree instead: identical content ends the run with a notice and no push, and different content fails with both tree IDs so the maintainer knows to bump the version.
Replace the Any-typed YAML dicts in the workflow tests with a recursive JsonValue alias. load_yaml validates the parsed document once with a TypeIs guard, and small accessors (at, mapping, text, steps, step) narrow each read, so no test body touches Any. The guards get direct tests.
The LaTeX deploy writes gh-pages and the pre-commit mirror writes a branch and tag from a caller repository. Name the new untrusted input, the write each one holds, and the controls, so reports can be classified.
Every gate calls required-checks, including the maturin and JetBrains gates that used inline jq. Maturin allows only a skipped sdist job. Tool installs in the new reusable workflows take exclude-newer and exclude-newer-package like the existing Actions. Inputs use kebab-case, jobs declare permissions explicitly, and checkouts do not persist credentials. The mirror workflow authenticates pushes with gh auth setup-git instead of the persisted checkout token.
conftest keeps one typed YAML path (load_yaml with BaseLoader, at,
mapping, text, steps, step) and one step runner (run_workflow_step).
The runner expands ${{ }} from explicit values unless the env override
supplies the variable. Tests drop Any, cast and local loaders.
Inline the module-level pattern and parse arguments through a typed Namespace subclass. Output is unchanged.
Document the shared workflows and their gates, correct the Validate a Python release example, and rename the allowed-paths fragment to a feature because it adds an optional input.
bump-deps-index 1.19.0, which carries the per-package and per-group cutoffs the update workflow needs, was uploaded on 2026-11-04. The seven-day cutoff would reject it, so the bump step exempts that one package until 2026-11-11. The exemption sits in the step rather than an input so callers cannot drop it while the release is young.
Comment on lines
+50
to
+55
| - name: 📥 Check out repository | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.sha }} | ||
| persist-credentials: false | ||
| - name: 📩 Download maintenance patch |
Comment on lines
+147
to
+153
| - name: 📥 Check out repository | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.sha }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: 📩 Download maintenance patch |
Comment on lines
+74
to
+80
| - name: 📥 Check out repository | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| ref: ${{ github.sha }} | ||
| - name: 🐍 Install uv and Python |
The same path and mode check ran twice in maintenance pull requests, and update-python-dependencies ran the hooks twice to confirm stability. The writer now applies the exact bytes the read-only job verified by digest, and one hook run accepts the files it rewrites, since the pull request checks run the hooks on the result. Release patch paths and modes are one check in validate-release-patch. Upgrading release tools hands its pull request to open-maintenance-pr instead of keeping a second writer, so the commit subject is the pull request title and the commit-message input goes away. Independent installs and downloads run in background steps and join before the step that needs them.
Describe the single path and mode check, the single hook pass, and the writer's digest check. Remove the commit-message input from the upgrade section.
Writer jobs hold a release App token or contents write. A reusable workflow runs with its caller's event, so a pull_request, pull_request_target, workflow_run, issue_comment or merge_group caller could hand a writer pull request code. Each writer job now runs a first step that fails with an error naming the event, before any checkout or token step runs. Read-only jobs and the SHA pins are unchanged.
Each writer section says which caller events it refuses and which triggers to use. The threat model rows that called these triggers caller configuration now name the refusal, and a changelog fragment records the behaviour change for callers.
for more information, see https://pre-commit.ci
pre-commit-mirror-maker commits and tags each upstream version itself, so the job needs a Git identity before it runs. The old workflow also decided "nothing to commit" by diffing against its own commit, which skipped any version whose formatting was a no-op, and pushed only the last tag when one run mirrored several versions. The job now records the base commit, folds the README and formatting changes into the newest mirror commit, checks each new tag against origin, and pushes the branch with every new tag in one atomic push. It creates a release per tag.
The inline shell check runs shfmt with two-space indentation and does not indent case patterns. Reformat the case blocks of the six writer guards to match.
The step-order test predates the writer guard, which now runs before the checkout in the mirror job.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Callers of these workflows currently write the gate, release, dependency and build jobs themselves. This PR offers them as reusable workflows and composite actions. A caller adds one
uses:line, pins a release SHA and receives later fixes at its next pin. 🧩The change covers pull request gates and CodeQL, the release tail, automated dependency and maintenance pull requests, and builds for maturin extensions, JetBrains plugins, pre-commit mirrors and LaTeX documents.
Write tokens are minted only in jobs that run no project code. Read-only jobs check out the pull request and produce a patch or an artifact, and they check it against the paths the caller allows. A separate writer job verifies the digest of that output and then mints the GitHub App token that pushes or publishes. The gate is one composite action,
required-checks, so every gate applies the same rule to skipped jobs. The trade-off is that callers keep their own triggers, environments and publish steps, and pin a full commit SHA for each release.Caller-facing changes
<caller job> / 🛡️ Analyze <language>. Branch protection that requires the old names must require the new ones.ci.yaml.check.yamlis now the reusable tox check for libraries, with one job perincluderow.exclude-newerandexclude-newer-package, with the same defaults and meaning as the existing Actions. The default cutoff is7 days.[tool.nab]exception carries an expiry date. After it passes,refresh-release-lockandupgrade-release-toolsfail on it.build-maturin-wheelspasses when its sdist job is skipped, which happens whensdistis off.upgrade-release-toolsno longer takescommit-message. The pull request title is the commit subject.validate-release-patchtakes a caller-suppliedallowed-pathslist, so a caller can admit paths beyond the default.Temporary bump-deps-index exception
update-python-dependenciesrequiresbump-deps-index>=1.19. That release, which carries the per-package and per-group cutoffs the workflow needs, was uploaded on 2026-11-04, inside the seven-day cutoff. The bump step exempts that one package until 2026-11-11 so callers can use the workflow now. The exemption is fixed in the step, so callers cannot remove it by changing their inputs.CI cost
validatejob. The writer applies the bytes whose digestvalidaterecorded. Kept: that read-only check, the only check of what the writer may change, and the digest check before the App token is minted.update-python-dependenciesrunscooled-prekonce. Hooks that rewrite files pass into the pull request, whose checks run the hooks on the result. Kept: a hook failure that changed nothing still fails the job, so a real error does not open a pull request.upgrade-release-toolshands its pull request toopen-maintenance-prinstead of keeping its own writer. Kept: the same validation and token boundary.update-python-dependencies, and apt, tox and Python incheck.yaml.stage-jetbrains-marketplaceno longer repeats the mode check.release-creatorrejects an unknown mode before any upload.ui-testbuild, which needs the plugin's own build configuration, the latex Pages verification that the served bytes match, and the mirror tag comparison and atomic push, which guard writes.