Industry practices ยท Practice 6 of 10

Run only the UI suites a frontend change affects

Mono repo build tools compute a dependency graph across modules and use it to run only the targets a change can affect. Nx (nx affected), Turborepo (--filter with git ranges), Bazel, Pants and Buck2 all do this, and they pair the graph with a cache keyed on each target's inputs, so a target whose inputs have not changed returns its previous result without running. In this system the service builds and the frontend build and unit pipelines are not where the time goes, so the graph pays off when it is applied to the second wave: a commit runs the UI suites of the modules it can affect, and every other module keeps the UI pass it already has.

Fewer UI suites per commit, fewer flaky failures

Today every commit to the mono repo starts all 100 UI pipelines, and a candidate needs all of them to pass on one revision. With the graph, a module's UI pass stays valid for a later revision as long as none of that module's inputs changed in between, so a candidate needs each module to have passed on its current inputs, in whichever run that happened. A commit that affects k modules exposes the candidate to k chances of a flaky failure, and at the overview's 3% flake rate per pipeline the chance that all of them pass falls with k as follows:

UI suites a commit affectsChance all pass at 3% flake rate
585.9%
1073.7%
2054.4%
5021.8%
1004.8%

A suite that fails on a flake can be rerun on its own, and the passes of the other suites still stand, which gives the same effect as the sticky passes on a frozen release branch (practice 10) without cutting a branch. The wall-clock time of a run barely changes, because the UI pipelines already run in parallel and the slowest affected suite still sets the duration. The savings are in flaky failures, in agent time, and in queueing when several commits arrive during one run.

Reusing an earlier pass is safe only when the graph lists every input of a UI suite. A UI suite that waits on another module's UI pipeline in the second wave has that suite's run as an input as well, and the planner either adds the edge to the graph or the dependency is removed first. Configuration, translations, styles, feature-flag defaults and test data are inputs too, and the tool has to be told about them, for example through Nx namedInputs or Turborepo inputs, or a suite will keep a pass that its changed inputs no longer justify.

How much the platform folder lets the graph save

A commit that touches the shared platform folder affects every UI module, and the table above shows that such a commit gains nothing. The value of this practice therefore depends on the share of commits that touch the platform, which the git history gives directly: count the commits over the last few months whose changed files include a path under the platform folder, for example with git log --since=3.months --name-only --format=%H, and compare them with the total. If most commits touch the platform, the graph alone saves little until the platform is split.

Splitting the platform into smaller packages with narrow public entry points lets the graph show that a change to one package affects only the modules that import it. The packages to split first are the ones changed most often, which the same git history identifies. Versioning the platform packages goes further and lets modules upgrade on their own schedule, so a platform change runs the UI suites only of the modules that take the new version.

changed platform/ui-kit platform/http platform/auth ui-search ui-onboarding ui-orders ui-billing ui-profile affected: UI suite runs unaffected: earlier pass still counts
Affected UI suites over a split platform. A change to platform/ui-kit runs the UI suites of the three modules that import it, and the other two keep the pass they already have. With one undivided platform folder, the same change runs every suite.

Backend changes and the UI suites

The frontend graph cannot see the backend repositories. If the UI suites run against deployed services, a service change can break a UI suite whose frontend inputs did not change, and an earlier pass would hide it. To keep the reuse safe, the UI suites can stub the services at the network boundary (practice 4), which makes the frontend graph the complete list of a suite's inputs and leaves cross-service breakage to contract tests (practice 5). If a suite has to run against real services, a service's new version counts as an input of every UI module recorded as its consumer in the Pact Broker, and only those modules' suites rerun.

Test impact analysis inside a UI suite

The graph decides which modules' suites run, and test impact analysis then decides which tests inside a suite to run, which can shorten the slowest suites. Paul Hammant's article "The Rise of Test Impact Analysis" (martinfowler.com, 2017) traces the approach through Google's Blaze build system and its TAP and Testar tools, and through Microsoft, which coined the name and ships it in VSTest and Azure DevOps. The common mechanism records the production files each test exercises, using coverage instrumentation, and looks up the changed files of each commit in that map, running the matching tests together with new tests and the tests that failed last time.

For the UI suites the map would come from browser coverage per test, for example V8 or Istanbul coverage gathered through Playwright. Hammant's caveats apply directly:

Meta's "Predictive Test Selection" paper (2019) replaces the coverage map with a model trained on historical results. It reports catching over 99.9% of faulty changes while running about a third of the tests, and similar features exist in Launchable, Gradle Develocity and Azure DevOps. A model needs only test history and change metadata, which suits UI tests whose coverage is expensive to collect. Selection inside a suite is a later step for this system, to take once the module-level graph is in place and particular suites still dominate the run.

Choosing the graph tool

Nx and Turborepo derive the graph from the existing package and TypeScript configuration of a JavaScript mono repo, while Bazel needs BUILD files for every module. Google and Uber run their repositories on Bazel, and Uber's 2024 article "Continuous deployment for large monorepos", by Rasmus Vestergaard and Kasper Munck, uses the Bazel graph to decide which services each commit changes and to build and deploy only those. Service builds are not the bottleneck here, so Bazel's cross-language reach buys little, and Nx or Turborepo gives the same result for the frontend mono repo with a smaller migration.

Order of adoption

  1. Measure from the git history the share of commits that touch the platform folder and how many modules a typical commit affects.
  2. Declare the non-code inputs of each UI suite in the graph tool, and decide whether the suites stub the services or treat service versions as inputs.
  3. Run the UI suites of affected modules only, and let the release assembly (practice 1) read each module's latest UI pass on its current inputs.
  4. Split the platform packages that change most often.
  5. Add selection inside the suites that still dominate the run.

In GoCD

Features to use

  • A planner pipeline after the build stage runs nx affected, or the equivalent, stores the list of affected modules as an artifact, and starts only those modules' UI pipelines through the scheduling API, which keeps one GoCD pipeline and one dashboard row per module.
  • Git materials take path filters, and with invert_filter a pipeline triggers only on changes under the listed paths. Path filters cannot follow dependencies, so every UI pipeline needs the platform paths in its filter, and a platform change still triggers all of them. A UI pipeline with an upstream UI pipeline runs whenever that pipeline passes, even when its own filter excludes the change, so these dependencies have to be removed or reproduced in the planner before the planner can skip a module.
  • A job with run_instance_count runs several copies in parallel with an index in GO_JOB_RUN_INDEX, which suits sharding a long UI suite across agents.

Gaps

  • GoCD's fan-in expects every upstream pipeline to have run against the same revision of the shared mono repo material, and a UI pipeline that was not started for a revision has no run there. The release decision has to read each module's latest pass through the API (practice 1) and cannot be a fan-in over all 100 UI pipelines.
  • GoCD does not know which inputs an earlier pass covered. The planner has to record, with each UI pass, the input hash or revision range it was valid for, so the release check can tell a pass that still applies from a stale one.
  • GoCD stores no coverage maps or test-selection data, and has no build cache. The map lives in a repository or a test-impact product, and the cache comes from the build tool, such as Nx's or Turborepo's remote cache.

Pros and cons for this system

Pros

  • A commit that affects a few modules exposes the candidate to a few chances of a flaky failure instead of 100.
  • A flaky suite is rerun on its own while the other passes stand, without a release branch.
  • Fewer UI runs per commit free agents, so commits that arrive during a run wait less.
  • The measurements in step 1 show which platform packages cause the widest reruns, which tells the platform team what to split.

Cons

  • An input missing from the graph lets a suite keep a pass it no longer deserves, and the defect surfaces only in a later full run of the UI suites.
  • While the platform folder stays undivided, any commit that touches it still runs all 100 suites.
  • UI suites that run against real services need service versions modelled as inputs, or a backend change goes untested in the UI.
  • The run duration stays set by the slowest affected suite, so a commit gets feedback no sooner than today.
  • The planner pipeline and the pass-validity record are new code that someone has to own.

Sources

Addresses wide blast radius from shared code, large batch size, CI theatre.

Sources Nrwl, Nx documentation "Run Only Tasks Affected by a PR"; Vercel, Turborepo documentation; Google, Bazel; Paul Hammant on martinfowler.com, "The Rise of Test Impact Analysis" (2017); Uber, Rasmus Vestergaard and Kasper Munck, "Continuous deployment for large monorepos" (2024); Meta, Mateusz Machalica, Alex Samylkin, Meredith Porth and Satish Chandra, "Predictive Test Selection" (ICSE-SEIP 2019) and the accompanying engineering blog post; Launchable; Gradle Develocity.