We hit a wall a few months back. GitHub Actions minutes were burning at roughly 5,000 a day, and it wasn't because the pipeline was doing anything fancy. It was doing everything, every time. Full backend suite, full frontend suite, migrations, security scans, all triggered on every push to every PR.
The instinct was obvious: run the full test suite locally before opening a PR, then let GitHub Actions just handle high-level smokes and end-to-end checks. Cheaper, faster, less quota burned.
That instinct is half right. The other half is where the real design work lives.
Local-first testing doesn't replace CI, it reshapes it
Pushing developers to run tests locally before a PR is good practice under any workflow -- GitHub Actions and GitLab CI/CD both assume some amount of pre-commit hygiene. But "local-first" only works if CI still catches what local can't: environment drift, container quirks, integration surfaces a laptop doesn't have.
We learned this the hard way with a real-Postgres integration tier. A few tests passed locally but failed inside the CI worker container -- different Postgres version, different connection pooling behavior, subtle timing issues that never show up on a dev machine. That's precisely the class of bug a "just smoke test it" pipeline would let straight through to main.
So the answer isn't "local replaces CI." It's tiered CI: fast unit and lint checks that gate every PR, a slower integration tier that only runs when the diff touches something that matters, and a full regression pass reserved for merges to main or a nightly schedule. This is the same idea covered in our production-ready CI/CD pipelines piece -- the goal isn't fewer checks, it's the right checks at the right stage.
Watch for triggers that silently don't fire
Tiering only works if triggers actually fire. We've seen a branch open a PR and get zero workflow runs -- not a failure, just nothing, because of a GitHub webhook race on branch creation. If your dashboard shows green because CI never ran rather than because CI passed, you have a worse problem than slow pipelines. Branch protection rules need to require a status context report success, not sit in a permanently "pending" or "expected" state -- a docs-only diff should still make test-backend and migrations-smoke report a real pass, not a shrug.
Parallelize, but verify the parallelism
Cutting CI time isn't just about skipping tests, it's about running the ones you keep faster. We tried pytest --forked -n auto for parallel test execution and hit an AttributeError that cost more debugging time than it saved. Switching to plain xdist parallelization gave real time savings without the singleton-pollution failures the forked mode introduced. Small tooling decision, real quota impact -- worth benchmarking before you commit to a parallelization strategy, not after.
Keep the pipeline and its documentation honest
CI rot isn't only about wasted minutes. It's about drift between what your docs say the pipeline does and what it actually does. We found a doc referencing .github/workflows/ci.yml that didn't exist -- the real workflows were split into unit-tests.yml, integration-db.yml, and others, renamed at some point without anyone updating the architecture doc. That kind of drift is the same failure mode we wrote about in Preventing Schema Drift in CI Pipelines -- silent, invisible, and expensive the day someone relies on stale information to debug a production incident.
Security hygiene belongs in this same bucket. Token generation for GitHub Apps via actions/create-github-app-token needs PKCS8-formatted keys, not the PKCS1 format some tools export by default -- a mismatch fails silently until a scheduled job tries to authenticate and can't. If you're managing org-level app installs with scoped repository access, verify your key format before you trust the automation.
The real fix isn't fewer tests, it's better placement
Cutting your Actions bill by gutting your test suite trades one problem for a worse one. The fix is architectural: fast checks gate PRs, heavier checks gate merges, integration tests run in environments that actually match production, and your triggers and docs stay honest about what's really running. We've hit ghost failures from exactly this kind of misconfiguration before, as covered in Hunting Ghost 503s and Pipeline Halts -- the pattern repeats because the underlying discipline, not the specific bug, is what's usually missing.



Top comments (0)