Split CI jobs for attributable failure and minimal dependencies
This is a principle
A reusable technical claim: something I would want true in any of my work.
Claim. Split a CI workflow into separate jobs along the lines where a failure should be attributable, and give each job only the toolchain it actually uses. A red check should name the culprit by its job, and no job should install a dependency it does not run.
When to apply. Any CI workflow covering more than one independent concern - multiple languages, multiple test layers, build vs. lint. Not a single-purpose pipeline where one linear sequence is the whole job.
The two rules.
- Split along failure-attribution lines. One job per independently-failing
concern. Node-script tests and Python-script tests become
node_testsandpython_tests, not onefastjob running both - so a red X names the layer at fault before you open the log. The split axis is what could fail independently, not what runs fast; speed is a side effect, attribution is the goal. - Give each job only its own dependencies. The node job sets up node and no uv; the python job sets up uv and no node. A combined job that installs both toolchains couples them: the python layer waits on a node install it never uses, and a failure in either setup taints both concerns. Minimal per-job setup is the CI form of single-responsibility.
Why. CI's product is a signal, and a signal is only useful if it localizes. A single job spanning two languages gives one bit ("something broke") where two jobs give two ("the python layer broke"); the reader skips straight to the failing layer. Minimal dependencies compound this: each job's setup is smaller, faster, and cannot fail for a reason unrelated to what it tests - a wedged node install can no longer redden a pure-python concern. The jobs also parallelize for free once decoupled, but that is the dividend, not the reason.
Snippet (coupled → split).
# Coupled: one job, both toolchains, failure names neither layer
jobs:
fast:
steps:
- uses: actions/setup-node@…
- uses: astral-sh/setup-uv@…
- run: make test_node_scripts
- run: make test_python_scripts
# Split: one job per attributable concern, each with only its own setup
jobs:
node_tests:
steps:
- uses: actions/setup-node@…
- run: make test_node_scripts
python_tests:
steps:
- uses: astral-sh/setup-uv@…
- run: make test_python_scripts
How enforced. Review the workflow by asking, of each job: if this goes red,
does its name tell me what broke? And: does it install anything it does not run?
Split on the first, trim on the second. Pairs with Bound every CI job with an explicit timeout (a per-job timeout is only attributable once jobs are split) and Run CI steps under a strict shell (errexit, pipefail). Earned splitting a combined fast CI job into node-only and python-only jobs, each carrying just its own toolchain.
Backlinks¶
The following pages link to this page: