Shared workflows for the QuantumKitHub Julia packages
GitHub does not cancel running workflows when a PR is closed or merged, and CI runners are shared across the whole organization.
All reusable workflows here therefore skip every job when triggered by a closed PR event, so that callers can use such an event purely to cancel the outstanding runs.
To make use of this, every caller workflow should:
-
Listen to the
closedevent (andready_for_review, so that undrafting a PR runs the full test suite), next to the defaultopened,synchronizeandreopenedevents:on: pull_request: types: [opened, synchronize, reopened, ready_for_review, closed]
-
Define a concurrency group that cancels in-progress runs for PRs, so that a new push (or closing the PR) cancels the previous run:
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }}
The concurrency group has to be defined in the caller: a group defined inside a reusable workflow would either clash with the caller's group or cancel sibling matrix jobs.
Additionally, consider:
- running a reduced matrix on draft PRs (see the
fastinput of the Test Groups workflow); - restricting
macos-latestandwindows-latestrunners, which have much lower organization-wide concurrency limits, to the latest Julia version on pushes tomain(the default for the Test Groups workflow); - using
paths-ignorefor changes that cannot affect the tests (e.g.docs/**,*.md) on workflows that are not required status checks — skipped required checks block merging.
The TestGroups workflow auto-discovers test groups from subdirectories of test/ and runs them in parallel across a matrix of Julia versions and operating systems (see Operating systems). Each subdirectory becomes a separate parallel job; groups are passed to the test suite via test_args and dispatched by ParallelTestRunner.jl.
Organize your test/ directory with one subfolder per test group. Place shared setup code in a test/setup/ directory (excluded by default):
test/
├── setup/ # shared utilities, excluded from test groups
├── core/
│ └── runtests.jl
├── extensions/
│ └── runtests.jl
└── runtests.jl # ParallelTestRunner.jl entry point
A minimal test/runtests.jl using ParallelTestRunner.jl:
using ParallelTestRunner
ParallelTestRunner.runtests()name: "Tests"
on:
push:
branches:
- main
tags: '*'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
tests:
uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/TestGroups.yml@main"
with:
fast: ${{ github.event.pull_request.draft == true }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}Add ci-success as a required status check in your repository's branch protection rules. This single stable check name reflects the combined result of all parallel test jobs regardless of how many groups exist.
| Input | Type | Default | Description |
|---|---|---|---|
exclude |
string | '["setup", "gpu"]' |
JSON array of test/ subdirectory names to exclude from discovery |
julia-version |
string | '["min", "1"]' |
JSON array of Julia versions to test on ubuntu-latest |
extra-os |
string | '["macos-latest", "windows-latest"]' |
JSON array of additional runner OSes, tested with julia 1 only |
extra-os-on-prs |
boolean | false |
Also test on extra-os for pull requests |
fast |
boolean | false |
Collapse matrix to ubuntu-latest + julia 1, append --fast to test args |
staged |
boolean | false |
Only run the extra-os jobs once all ubuntu jobs have passed |
nthreads |
number | 2 |
Julia thread count per job |
timeout-minutes |
number | 60 |
Per-job timeout |
localregistry |
string | "" |
Newline-separated local registry URLs |
cache |
boolean | true |
Enable julia-actions/cache |
buildpkg |
boolean | true |
Enable julia-actions/julia-buildpkg |
coverage |
boolean | true |
Collect and upload coverage (only on ubuntu-latest + julia 1) |
coverage-directories |
string | "src,ext" |
Directories for julia-processcoverage |
Secrets: CODECOV_TOKEN (optional, only needed when coverage: true)
When fast: true, the matrix collapses to a single OS and Julia version and --fast is appended to each group's test args. Wire it to draft PR detection for quick feedback during development:
with:
fast: ${{ github.event.pull_request.draft == true }}ParallelTestRunner.jl passes --fast through to individual test files, which can use it to skip slow or expensive tests.
macOS and Windows runners are scarce: GitHub only allows a handful of concurrent macOS jobs per organization.
Therefore, every Julia version in julia-version is tested on ubuntu-latest, while the systems in extra-os are only tested with julia 1, and only on pushes to the default branch and manual (workflow_dispatch) runs.
Tags are not included, since they normally point to a commit that was already tested on the default branch.
Packages where platform-specific failures are common (e.g. those relying heavily on Enzyme) can also run the extra-os jobs on PRs.
Combine this with staged to only start them once every ubuntu job has passed, so that broken commits and commits superseded by a new push never occupy the scarce runners:
with:
fast: ${{ github.event.pull_request.draft == true }}
extra-os-on-prs: true
staged: ${{ github.event_name == 'pull_request' }}Override exclude to control which subdirectories are skipped:
with:
exclude: '["setup", "gpu", "cuda"]'The Tests workflow is designed to run the tests suite for Julia packages.
The workflow works best with a runtests.jl script that looks like this:
using Test
# check if user supplied args
pat = r"(?:--group=)(\w+)"
arg_id = findfirst(contains(pat), ARGS)
const GROUP = if isnothing(arg_id)
uppercase(get(ENV, "GROUP", "ALL"))
else
uppercase(only(match(pat, ARGS[arg_id]).captures))
end
@time begin
if GROUP == "ALL" || GROUP == "CORE"
@time include("test_core1.jl")
@time include("test_core2.jl")
# ...
end
if GROUP == "ALL" || GROUP == "OPTIONAL"
@time include("test_optional1.jl")
@time include("test_optional2.jl")
# ...
end
# ...
endAn example workflow that uses this script is:
name: Tests
on:
push:
branches:
- 'master'
- 'main'
- 'release-'
tags: '*'
paths-ignore:
- 'docs/**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Cancel intermediate builds (and builds of closed PRs): only if it is a pull request build.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
tests:
name: "Tests"
strategy:
fail-fast: false
matrix:
version:
- 'lts' # minimal supported version
- '1' # latest released Julia version
# optionally, you can specify the group of tests to run
# this uses multiple jobs to run the tests in parallel
# if not specified, all tests will be run
group:
- 'core'
- 'optional'
os:
- ubuntu-latest
- macOS-latest
- windows-latest
uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main"
with:
group: "${{ matrix.group }}"
julia-version: "${{ matrix.version }}"
os: "${{ matrix.os }}"
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}The documentation workflow is designed to build and deploy the documentation for Julia packages.
The workflow works best with a docs/make.jl script that looks like this:
using MyPackage
using Documenter
makedocs(; kwargs...)
deploydocs(; kwargs...)An example workflow that uses this script is:
name: "Documentation"
on:
push:
branches:
- main
tags: '*'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
schedule:
- cron: '1 4 * * 4'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Never cancel deployments from `main` or tags
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-and-deploy-docs:
name: "Documentation"
uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Documentation.yml@main"
secrets: "inherit"The formatting workflow is designed to run Runic on Julia packages and verify the formatting.
name: "Format Check"
on:
push:
branches:
- 'main'
tags: '*'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
format-check:
name: "Format Check"
uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/FormatCheck.yml@main"The LiterateCheck workflow is designed to keep the README of Julia packages up to date. The workflow would look like:
name: "Literate Check"
on:
push:
branches:
- 'main'
tags: '*'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
format-check:
name: "Literate Check"
uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/LiterateCheck.yml@main"