Shared, reusable GitHub Actions workflows for ucgmsim Python repos:
ci.yml—ruff(check + format),deptry,npdlint(numpydoc-style docstrings),pytest, andty(type checking) as five parallel jobs, plus arustjob (cargo fmt/clippy/test) that only appears for repos containing aCargo.toml.claude-review.yml— an on-demand Claude PR review, triggered by a@claude reviewcomment (never automatically on PR open/push).
In the consuming repo, replace the repo's own ruff.yml, deptry.yml,
numpydoc.yml, pytest.yml, and types.yml workflows with a single file,
e.g. .github/workflows/ci.yml:
name: CI
on: [pull_request]
jobs:
ci:
uses: ucgmsim/meta-ci-action/.github/workflows/ci.yml@v1
with:
# package-dir: qcore # unset once [tool.npdlint] has `include`
# system-packages: "gmt libgmt-dev ghostscript"
# uv-extra-args: "--all-groups --all-extras"
# exclude-init: false # lint __init__.py too
# enable-coverage: true
# cov-package: qcore
# cov-fail-under: 95| Input | Default | Purpose |
|---|---|---|
python-version |
"3.13" |
Interpreter version for the docstring job's setup-python step. It parses the code under test, so it must be at least as new as the syntax that code uses. |
package-dir |
"" |
Top-level package directory to lint for docstrings, e.g. qcore, workflow, IM. No longer required: leave it unset once [tool.npdlint] include names the same paths. |
system-packages |
"" |
Space-separated apt packages installed before the deptry/pytest/typecheck jobs (e.g. native libs like GMT or GDAL). |
uv-extra-args |
"--all-extras --dev" |
Extra flags passed to uv sync in the deptry/pytest/typecheck jobs. |
numpydoc-extra-excludes |
"" |
Deprecated. Extra -E fdfind exclude fragments, e.g. -E ccldpy.py. Still honoured — each becomes an npdlint --extend-exclude — so a caller written for the old pipeline needs no edit, but [tool.npdlint] extend-exclude is the place for these now. |
exclude-init |
true |
Skip __init__.py in the docstring job, as the fdfind pipeline always did. Set false once [tool.npdlint] owns the exclusions and you want __init__.py linted too. |
pytest-paths |
"tests" |
Paths passed to pytest. Set to "" to fall through to [tool.pytest.ini_options] testpaths. |
rust-features |
"" |
Space-separated cargo feature sets; each gets its own cargo test --features <set> run. Ignored when the repo has no Cargo.toml. |
enable-coverage |
false |
When true, runs pytest with --cov=<cov-package> and gates on cov-fail-under. |
cov-package |
"" |
Package name passed to --cov=. Required when enable-coverage is true. |
cov-fail-under |
95 |
Coverage percentage threshold passed to coverage report --fail-under=. |
Nothing needs enabling. A detect job checks the repo out and looks for a
Cargo.toml; when it finds one:
- a
rustjob runscargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo test,cargo test --doc, and onecargo test --features <set>per entry inrust-features; - the
deptry,pytestandtypecheckjobs additionally install a stable Rust toolchain and aSwatinem/rust-cache@v2cache, sinceuv synchas to build the extension module in each of them.
For a maturin project, put the build profile in uv-extra-args — otherwise
every Python job compiles the crate with the release profile, which for a
crate using lto/codegen-units = 1 dominates the CI time:
jobs:
ci:
uses: ucgmsim/meta-ci-action/.github/workflows/ci.yml@main
with:
package-dir: nzcvm
uv-extra-args: "--all-extras --dev -C build-args=--profile=dev"
rust-features: "high_precision"
pytest-paths: ""Each job runs uv sync once and then uv run --no-sync, so the config
settings applied at sync time are the ones the tests run against and no step
silently rebuilds the crate.
claude-review.yml is a workflow_call-only reusable workflow — it has no
trigger of its own. Each consuming repo needs its own thin wrapper carrying
the actual issue_comment trigger, e.g. .github/workflows/claude-review.yml:
name: Claude PR Review
on:
issue_comment:
types: [created]
jobs:
review:
uses: ucgmsim/meta-ci-action/.github/workflows/claude-review.yml@v1
secrets:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}The reusable workflow itself gates on contains(github.event.comment.body, '@claude review') and only runs on PR comments — it does not run on PR
creation, push, or any other event. CLAUDE_CODE_OAUTH_TOKEN must be set as
a secret in the consuming repo (or its org) and passed through explicitly,
since reusable workflows don't inherit secrets automatically unless declared.
- Args live in
pyproject.toml, not the workflow. Tool behavior (ruff rules, deptry dev-dependency groups,tyexcludes) should be configured via each repo's own[tool.*]sections, so this workflow stays argument-free where possible.package-dirandnumpydoc-extra-excludesused to be the exceptions, becausenumpydoc's CLI could express neither a set of paths to walk nor a path to skip.npdlintreads both from[tool.npdlint], so they are now optional and on their way out: a fully migrated repo calls this workflow with no docstring inputs at all. - Coverage is opt-in. Set
enable-coverage: true(pluscov-package) to add a--covrun and acoverage report --fail-under=gate to the pytest job. Leftfalse, pytest just runspytest testswith no coverage instrumentation at all — matches repos that don't want the extra CI time or don't have a coverage target yet.
-
Move hardcoded CLI args into
pyproject.toml. Check the repo's existingruff.yml/deptry.yml/types.yml/etc. for flags baked into therun:steps (e.g.ty --exclude,deptry's dev-dependency groups) and move them into the matching[tool.*]section instead, so the shared workflow can invoke each tool without repo-specific arguments. -
Identify what can't move to config. Very little has to stay a
with:input now. Docstring paths and excludes belong in[tool.npdlint]:[tool.npdlint] include = ["qcore"] extend-exclude = ["**/__init__.py", "ccldpy.py"]
With that in place, drop
package-dirandnumpydoc-extra-excludesfrom theci.ymlcall and setexclude-init: false, so the exclusions live in one file rather than two. -
Add the caller workflow. Create
.github/workflows/ci.ymlin the consuming repo per the Usage example above, setting only the inputs that differ from the defaults. -
Delete the superseded workflows (whichever of
ruff.yml,deptry.yml,numpydoc.yml,pytest.yml,types.ymlthe repo has). Leave anything unrelated to these five tools untouched. -
Validate before merging. Open a draft PR and confirm each job passes (or fails the same way the old workflow did) before relying on it.
Adopting claude-review.yml is independent of the above — it's a separate
opt-in workflow, not part of the ci.yml migration.