# When is it safe for CI to test only packages affected by a change?

> Affected-only CI is safe when your dependency model captures every input that can change test outcomes, and when CI widens scope on changes the graph cannot model.

Canonical URL: https://www.devobs.io/articles/qa-affected-build-selection/
By: Amara Okafor
Published: 2026-07-21T05:28:08.597Z
Updated: 2026-09-06T08:31:04.426Z
Section: Architecture

Affected-only CI is safe when your build system can answer not just “what changed,” but “what could this change invalidate?” That means a complete dependency graph, complete task inputs, and explicit fallback rules for changes the graph cannot represent. If shared config, generators, lockfiles, environment values, or implicit dependencies can alter behavior without marking downstream packages as affected, affected-only testing is not a safe primary gate.

## What does graph completeness actually mean?

Graph completeness means every package or test that could fail because of a change is reachable from that change in your model. Nx’s affected workflow is built for this: it lets you “only run tasks on projects that were affected by a particular PR” ([Nx documentation](https://nx.dev/docs/kb/reduce-waste)). Bazel’s query tools show the same idea from another angle: `deps(...)` finds what a target needs to build, `rdeps(...)` finds reverse dependencies, and `allpaths(...)` or `somepath(...)` help explain why an edge exists ([Bazel query guide](https://bazel.build/query/guide)).

The catch is that source imports are only part of the graph. Real CI outcomes also depend on shared compiler and test configuration, generated code inputs, lockfiles, and environment or runtime values. If those inputs are outside the model, your affected set is incomplete even if the source dependency graph looks clean.

## Which changes should trigger fallback rules?

Treat affected-only as the default path, then expand scope when a change touches inputs that are broader than normal code dependencies.

A practical rule set is:

- shared lint, test, compiler, or CI config changed: run every project that consumes that config;
- generator code, templates, or schemas changed: run all generated consumers, or fall back to a full run if consumers are not tracked;
- lockfile changed: run all packages resolved through that lockfile and boundary-crossing integration tests;
- environment or runtime-dependent task definitions changed: run all tasks that rely on those inputs;
- implicit or dynamic dependencies detected: prefer a wider run.

Bazel explicitly warns that some rules carry “implicit dependencies” that are “not mentioned in the build file, but added in by the build tool” ([Bazel query guide](https://bazel.build/query/guide)). That is exactly the class of risk that can produce false negatives if your CI selection logic only follows obvious package edges.

## What does a concrete decision checklist look like?

Suppose `web` depends on `ui`, and `ui` includes files generated from `design-tokens.json`.

If a [pull request](https://www.devobs.io/articles/small-pull-request-dependency-stacks/) changes a page inside `web`, affected-only testing for `web` is usually fine.

If it changes a component inside `ui`, run `ui` plus every reverse dependency, including `web`.

If it changes `design-tokens.json` or the generator that emits `ui` artifacts, do not trust a plain import graph. Either declare explicit edges from generator inputs to generated consumers, or widen the run.

Before rollout, verify this checklist:

- reverse dependencies are queryable;
- shared config files are mapped to consumers;
- generated artifacts have traceable inputs;
- lockfile policy is explicit;
- the affected set is computed from the exact revision under test, before job fan-out.

That ordering boundary matters. A preflight diff is not enough if later jobs read different generated state or configuration than the revision used to compute “affected.”

## How do we know affected-only CI is not missing failures?

Measure it. Keep affected-only on pull requests, then run the full suite on the default branch on a schedule. Compare outcomes and classify every failure found only by the full run: missing graph edge, undeclared shared input, generator gap, nondeterministic test, or environment drift. If that gap persists, your repository is not ready to rely on affected-only for correctness.

## What should a team verify before adopting this workflow?

Use it when dependencies are explicit and auditable. Do not use it as the main gate for highly dynamic plugin loading, heavy reflection, runtime discovery of tests, or repositories where shared changes routinely invalidate nearly everything. The next step is to codify fallback rules in one repo area, then compare a month of affected runs against scheduled full runs and close every false-negative class you find.

Reviewed: 2026-09-05

## Source references

- <https://nx.dev/docs/kb/reduce-waste>
- <https://bazel.build/query/guide>
