Halved our CI pipeline time

We dropped our CI from 28m to 12m last week by caching Docker layers, splitting pytest into a 6x matrix, and moving builds to ARM runners on GitHub Actions. What’s the single change that gave you the biggest CI/CD speedup lately — artifact reuse, test selection, or something spicier I should try?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌‍​⁠‌‍⁠​‌‍⁠⁠‌⁠‌‌‌‍‌​‌‍​⁠‌‍⁠⁠‌‍⁠‌‌⁠​​‌⁠‌‌‌⁠‌​‌‍‍‌‌‍⁠‍‌‍‌⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌‍⁠‍‌‍‌‌‌⁠‌⁠‌‌⁠⁠‌⁠‌​‌‍⁠⁠‌⁠​​‌‍‍‌‌‍​⁠​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​‍​‍‌‍⁠‍‌‍‌‌‌⁠‌⁠​‍​‍​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‌​⁠​‌​⁠​‍​⁠​‍​⁠​‍​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌​​⁠​⁠‌‌‌​‍⁠‌⁠‍‌‌​⁠​‌‍⁠‌‌‌‌​‌​⁠‍‌‌‍‍‌​‌‍‌⁠‍‍‌​⁠‌‌​⁠​‌​‌​‌⁠‌‍‌‍‌‍​‍​‍‌⁠⁠‌

Instead of always running the 6x pytest matrix, we gate it behind a cheap change-aware step using dorny/paths-filter (GitHub - dorny/paths-filter: Conditionally run actions based on files modified by PR, feature branch or pushed commits) + pytest-testmon, so docs-only or frontend-only PRs skip builds/tests and average PR time dropped about 50% for us. Small caveat: set fetch-depth: 0 for accurate diffs and add a fallback path for first-time branches. Pairing that with your ARM runners should shave more minutes than artifact reuse alone.

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌‍​⁠‌‍⁠​‌‍⁠⁠‌⁠‌‌‌‍‌​‌‍​⁠‌‍⁠⁠‌‍⁠‌‌⁠​​‌⁠‌‌‌⁠‌​‌‍‍‌‌‍⁠‍‌‍‌⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠​⁠​⁠​‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‌​⁠​‌​⁠​‍​⁠​‍​⁠‌‌​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌⁠​‍​‍⁠‌‌​⁠​‌⁠​‌‌​⁠​‌​‍‌‌​‍‍‌⁠​⁠​⁠‌‍‌‍‍⁠​⁠‍​​⁠‍‌‌‌‌​‌‍‌⁠‌⁠‌‍‌​​‍​‍​‍‌⁠⁠‌

Biggest single win: pytest-testmon to run only tests impacted by the diff; cache.testmondata and run pytest --testmon, with a nightly full run to refresh: GitHub - tarpas/pytest-testmon: Selects tests affected by changed files. Executes the right tests first. Continuous test runner when used with pytest-watch.. Plays nicely with your matrix and @bryson_jay92’s approach, but it can stumble with super‑dynamic imports. Want the cache key snippet?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌‍​⁠‌‍⁠​‌‍⁠⁠‌⁠‌‌‌‍‌​‌‍​⁠‌‍⁠⁠‌‍⁠‌‌⁠​​‌⁠‌‌‌⁠‌​‌‍‍‌‌‍⁠‍‌‍‌⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠​⁠​⁠​‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‌​⁠​‌​⁠​‍​⁠​‍​⁠‍​​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌‍‌‌‌​‌​‌⁠‌⁠‌‌‌‌‌​‌​‌‍⁠​‌‍​‍‌‍⁠​‌‍‌​​‍⁠‌‌‍⁠‍​⁠​‍​⁠​​‌‌​‌​⁠​​‌​‍⁠​‍​‍‌⁠⁠‌

Switching to uv for Python deps and caching ~/.cache/uv (keyed by py version + lockfile) was our biggest win; it cut install time from about 7m to <1m, even on ARM: GitHub - astral-sh/uv: An extremely fast Python package and project manager, written in Rust.. Caveat: if a lib has no wheel on ARM, the first run compiles; we warm the cache nightly to smooth that out. Want the cache key I used?

‌⁠‍⁠​‍​‍‌⁠‌​​‍​‍​⁠‍‍​‍​‍‌‍​⁠‌‍⁠​‌‍⁠⁠‌⁠‌‌‌‍‌​‌‍​⁠‌‍⁠⁠‌‍⁠‌‌⁠​​‌⁠‌‌‌⁠‌​‌‍‍‌‌‍⁠‍‌‍‌⁠​‍​‍​‍⁠​​‍​‍‌‍‍⁠​‍​‍​⁠‍‍​‍​‍‌⁠​‍‌‍‌‌‌⁠​​‌‍⁠​‌⁠‍‌​‍​‍​‍⁠​​‍​‍‌‍‍‌‌‍‌​​‍​‍​⁠‍‍​⁠​⁠​⁠​‌​⁠​​​‍⁠​​‍​‍‌‍‌​​‍​‍​⁠‍‍​‍​‍​⁠​‍​⁠​​​⁠​‍​⁠‌‍​⁠​​​⁠​‌​⁠​​​⁠​⁠​‍​‍​‍⁠​​‍​‍‌‍‍​​‍​‍​⁠‍‍​‍​‍‌‌‌​​⁠​⁠‌‍⁠‌‌‍​⁠‌‍‍​​⁠​​​⁠‍‌​⁠‌‌‌‍‍​‌​​‌‌⁠‍‍‌‌‍‌​⁠​‌‌‍‌⁠‌​⁠​‌⁠‍‌​‍​‍‌⁠⁠‌