Size CI matrices by event to cut GitHub Actions runner minutes - #931
Conversation
A full CI run costs ~425 Linux-equivalent runner minutes across 15 jobs. macOS bills at 10x and Windows at 2x Linux, so the four jobs on those two platforms accounted for ~70% of that while catching essentially nothing Linux did not: macOS 2 jobs 21.8 wall min 218 weighted min 51% Windows 2 jobs 39.4 wall min 79 weighted min 19% Linux 11 jobs 128.2 wall min 128 weighted min 30% So the matrices are now event-dependent. A pull request runs Linux with the oldest and newest supported Python. The weekly schedule and manual dispatch keep the full matrix: every platform, Python 3.10 through 3.13. A pull request that needs the full matrix before merging opts in with the ``full-ci`` label (labels come from the event payload, so label first and then re-run). That takes a pull request from 15 jobs / ~425 weighted minutes to 8 jobs / ~90, a ~79% reduction, without changing what the weekly run covers. Alongside that: * Coverage was measured and uploaded by all four ``maximum-test-conda`` entries. Instrumentation slows the suite measurably and four identical reports say nothing the newest Python alone does not, so only the 3.13 entry measures and uploads. * Tutorial datasets are restored from a cache keyed on ``tutorial.py`` instead of re-downloaded per job, which also removes a recurring source of network flakiness. ``setup-python`` now caches pip wheels, which is worth several minutes on the Windows and macOS installs. * Every job has a ``timeout-minutes`` cap. Without one a hung macOS job runs to the 6h default, which alone would cost 3600 weighted minutes. * ``benchmarks.yml`` gained the concurrency group the other workflows already had, so superseded runs are cancelled. * The ``maximum-test-conda`` matrix used ``include:`` to carry an ``env`` value whose only consumer was an "Install bleeding edge" step that could never run (the value never matched its condition). Both are gone; ``esmpy`` moved into ``create-args`` directly. Separately, ``tool.pytest.ini_options.testpaths`` still pointed at ``climpred/tests``, the pre-src-layout location. pytest found nothing there, warned, and silently rescanned the whole repository on every run: PytestConfigWarning: No files were found in testpaths; consider removing or adjusting your testpaths configuration. Searching recursively from the current directory instead. Pointed at ``src/climpred/tests``. ``src/climpred/conftest.py`` sits above that directory and so is still collected; verified on a minimal reproduction of the layout. Verified with ``actionlint`` (no new findings) and ``pre-commit run --all-files`` (all hooks pass, including workflow schema validation). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVFyiToC1QjAJdDHecCfDX
The key read:
ubuntu-latest-${{ runner.arch }}-py${{ matrix.python-version }}-${{ env.TODAY }}-${{ hashFiles(env.CONDA_ENV_FILE) }}
``env.TODAY`` and ``env.CONDA_ENV_FILE`` are never defined in this workflow
-- they came over from xarray's, which sets them in an earlier step -- so
both interpolated to empty.
That was misleading rather than harmful: ``setup-micromamba`` treats the
input as a prefix only and appends a hash of the environment file, the
``create-args``, the environment name and the OS to it, so the environment
cache was already invalidated whenever ``ci/requirements/maximum-tests.yml``
or the Python version changed. What the custom prefix contributed was the
OS, arch and Python version the action appends anyway, plus two empty
variables.
Removing it leaves this job caching exactly like the three other conda
jobs, which pass no key at all.
Note that neither form rotates the cache on a timer, so an unchanged
environment file keeps its original solve. If the weekly run should re-solve
to pick up new dependency releases, that wants a deliberate date-based key
across all four conda jobs, not this line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVFyiToC1QjAJdDHecCfDX
The entries were written before the PR number was known. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVFyiToC1QjAJdDHecCfDX
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #931 +/- ##
=======================================
Coverage 91.27% 91.27%
=======================================
Files 59 59
Lines 6548 6548
=======================================
Hits 5977 5977
Misses 571 571 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CI green; merge blocked by branch protection — needs a settings changeRun 35607142259 is green: 9/9 jobs, all The PR nonetheless reports What I can see:
GitHub holds such a check as Expected — waiting for status to be reported, which blocks the merge indefinitely. What's needed (Settings → Branches →
I can't read the protection settings through my access, so I can't confirm which entries are actually pinned, and this is not something a push can fix — restoring those job names would undo the change this PR is for. If the block turns out to be something else entirely (a required review after all, or a rule I can't see), say so and I'll take it from there. Generated by Claude Code |
Description
A full CI run costs ~425 Linux-equivalent runner minutes across 15 jobs. Measured on run 35582791229 (the green run on
main), weighted by GitHub's billing multipliers — macOS 10×, Windows 2×:Four jobs — the macOS and Windows ones — were ~70% of the cost, and they catch essentially nothing Linux does not.
So the matrices are now event-dependent:
workflow_dispatchkeep the full matrix, unchanged: Linux, macOS and Windows, Python 3.10 through 3.13.full-cilabel. Labels come from the event payload, so label first, then re-run the workflow (or push again).Measured on this PR's own run (35607142259), all nine jobs on
ubuntu:ubuntu-slim)425 → 85.8 weighted minutes, a 80% reduction, and wall-clock 24.1 → 19.3 min, without changing what the weekly run covers.
Alongside that:
maximum-test-condaentries ran--covand uploaded to codecov. Codecov on this PR confirms the other three were redundant: 91.27%, 5977 hits / 6548 lines,+/- 0.00%— identical to the base commit's four-upload result. The saving is the three redundant uploads and their report processing; this run shows no measurable runtime difference from the instrumentation itself.tutorial.py) rather than re-downloaded in every job — 20–70 s per job, and one less flaky network step.setup-pythonnow caches pip wheels too; the Windows install alone took 4.5 min. Both caches were cold on this first run, so the benefit lands from the next run onward.timeout-minutescap. Without one, a hung macOS job runs to the 6 h default — 3600 weighted minutes from a single hang.benchmarks.ymlgained the concurrency group the other workflows already had, so superseded runs are cancelled.maximum-test-conda: aninclude:carrying anenvvalue whose only consumer was an "Install bleeding edge" step that could never fire, and a hand-rolledcache-environment-keyinterpolatingenv.TODAYandenv.CONDA_ENV_FILE, neither of which this workflow defines.setup-micromambaalready appends a hash of the environment file,create-args, environment name and OS to the cache key, so the custom prefix contributed nothing.Two bugs found on the way, both fixed here:
testpathspointed at the pre-src-layoutclimpred/tests. pytest found nothing there, warned, and silently rescanned the whole repository on every run:PytestConfigWarning: No files were found in testpaths; consider removing or adjusting your testpaths configuration. Searching recursively from the current directory instead.Nowsrc/climpred/tests.maximum-test-condacache key above.Type of change
How Has This Been Tested?
actionlinton all workflows: no new findings. The two it reports (ubuntu-slimunknown to actionlint 1.7.7, an untrusted-input expression inbenchmarks.yml) are pre-existing and untouched.pre-commit run --all-files: all hooks pass, includingcheck-jsonschema's GitHub workflow schema validation.The
testpathschange was verified on a minimal reproduction of the layout — the old value does fall back to a full-repo rescan, the new one collects the tests, andsrc/climpred/conftest.pysits above the testpath so its fixtures are still collected.The matrix reduction is verified by this PR's own run: nine jobs, all Linux, all green.
Tests added for
pytest, if necessary. — not applicable, CI configuration only.Checklist (while developing)
AGENTS.mdgained a Continuous Integration section describing the lean/full matrix policy and thefull-cilabel.Note for the reviewer
Branch protection needs updating before this can merge — see the comment below. CI is green and there is no conflict, but the PR reports
mergeable_state: blocked.One thing deliberately left alone: no conda cache key rotates on a timer, so an unchanged environment file keeps its original solve indefinitely. If the weekly run should re-solve to pick up new dependency releases, that wants a deliberate date-based key across all four conda jobs — a separate change, and one that costs minutes rather than saving them.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DVFyiToC1QjAJdDHecCfDX