Keep CI fast as AI coding speeds up

Keep CI fast as AI coding speeds up

~ 8 min read


In a repository containing one or more backend APIs and a TypeScript frontend built with React or Nuxt, a component change can trigger backend checks, database setup, test shards, and coverage checks for both layers. If the frontend checks are independent of the APIs, those backend jobs add no useful evidence about the component.

Use the pull request’s changed files and their dependencies to run related test groups for affected APIs and the frontend. Keep the full browser suite for application changes, and run all relevant suites whenever an input is shared or uncertain. The goal is to shorten feedback time and reduce compute while keeping the checks each change needs.

Running every suite on every change is a straightforward starting point. As the codebase and change volume grows, repeated CI work can become expensive. AI-assisted coding can increase that volume further. An agent can start another task while CI runs, but a late failure still interrupts work that has moved on.

Stacked pull requests can make review easier by splitting work into smaller changes. Updating an earlier PR can require rebasing later PRs and rerunning their checks, adding to the CI work for the stack if not filtered appropriately to the affected change chain.

Faster code generation does not guarantee faster delivery. In their March 2026 DORA analysis, Jessica Baolin and Nathen Harvey examine 1,110 open-ended responses from Google software engineers and describe how time saved generating code can move into auditing and verification. Selective CI addresses one part of that verification cost, where dependencies give us a sound reason to skip work.

Select suites by what the PR can affect

Consider a repository with its APIs under backend/ and its TypeScript application under frontend/. Frontend checks run formatting, linting, type checks, component tests, and a production build. If those checks use mocked API calls and need no backend-generated files, they can run without starting the APIs or running their suites. Backend checks can run independently when they have no dependency on frontend output.

Selection happens at suite level for each API. A change confined to backend/orders/ runs the complete orders API suites and coverage jobs. It can skip the suites for an independent backend/billing/ API. If billing depends on the changed orders behaviour, select its suites too.

The browser suite exercises the integrated application. Run the full suite for changes to any API or the frontend, rather than filtering it to the edited API or component. For this example, a conservative routing policy looks like this:

Changes in the PRAPI suites and PR coverageFrontend checksBrowser tests
Frontend onlySkipRunFull suite
One or more APIs onlyAffected APIsSkipFull suite
APIs and frontendAffected APIsRunFull suite
Shared configuration or unknown pathsAll APIsRunFull suite
Known documentation and guidance onlySkipSkipSkip

Documentation-only updates can pass CI without running API, frontend, or browser tests when the changed files have no effect on the application or its build. Run applicable documentation checks, and keep workflow linting, routing tests, and infrastructure safeguards on every PR ready for review. Final checks should accept the intentional application-test skips. Decide separately whether draft PRs run CI.

Nx’s affected-project documentation applies the same dependency principle to larger workspaces. It uses Git to identify changed files, then a project graph to find the projects they belong to and their dependants. Its default response to a changed package-manager lockfile is to mark every project affected as a failsafe.

A repository with separate backend and frontend directories can use simpler routing. If the frontend imports types or clients generated from an API schema, changes to that schema must also select frontend checks. The rules need to follow that dependency across directories.

Include everything that changes how code runs

Classify the whole application tree, including tests, assets, dependency manifests, and lockfiles. A frontend dependency update needs frontend checks even when no component changes. API schemas, database migrations, and backend configuration need backend checks even when no request handler changes.

Keep documentation exclusions narrow. Treating every *.md file as documentation would skip a Markdown fixture or a public asset inside an application directory. Known guidance files and docs/ can be excluded explicitly if they have no build dependencies; application content belongs to its owning suite.

Shared Docker configuration can alter every environment, so changes to it need all API suites, frontend checks, and the full browser suite. Give unknown paths the same treatment. This costs more when somebody adds a new root file, but it avoids requiring them to remember a CI rule before their change receives testing. Narrow the rule once the dependency is understood.

For Git-based detection, set BASE_SHA and HEAD_SHA to the PR’s full base and head commit IDs supplied by the workflow:

git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" --

The three-dot comparison covers changes since the merge base, rather than only the latest commit. Disabling rename detection includes both sides of a move, so moving a file between the frontend and backend triggers both. NUL-delimited paths preserve filenames containing spaces or newlines. These options are documented in the Git diff reference.

Fetch the history needed to resolve the merge base, using a full-history checkout if appropriate. Check the command’s exit status before publishing any routing outputs.

A failed diff must fail CI

Two separated colour fields share a narrow illuminated passage, with a dark interruption preventing an unchecked route through.

A detector must distinguish a successful empty diff from a failed command. In Bash, a Git command inside process substitution can fail without making the surrounding loop fail. A script that starts with application=false can then publish that value even though it never obtained the file list, allowing tests to be skipped after a detection failure.

One option is to invoke Git through Node’s execFileSync, which throws on a failed command. Publish flags only after detection succeeds. Add a final job that validates the detector’s result, requires explicit true or false flags, and checks that suite results match the decision.

For each selected suite, every required job must succeed. For an unselected suite, its jobs must be skipped intentionally. Missing outputs, cancellation, and unexpected skips fail the final check.

GitHub’s distinction between skipped workflows and skipped jobs matters here. A workflow skipped by path filters can leave required checks pending; a conditional job skip reports success. A dependent job can also be skipped after an upstream failure. GitHub documents these behaviours and recommends always() with needs for required checks that depend on other jobs.

Keep filtering inside the workflow. Give final checks stable names and use always() with needs to inspect their dependencies even after a failure. Require those final checks in branch protection and verify that they block a failing PR.

Measure waiting time and compute separately

Martin Fowler’s guidance on continuous integration recommends keeping the build fast and discusses the Extreme Programming guideline of a ten-minute build. Frequent integration makes even small delays worth addressing. Measure how much work CI does as well as how long developers wait.

Suppose four backend shards each take eight minutes and run in parallel. That is 32 runner-minutes, with about eight minutes of elapsed time if they start together. If the remaining frontend and browser jobs all finish within three minutes of the workflow starting, skipping the backend shards on a frontend-only PR saves about five minutes of elapsed time and 32 runner-minutes. This illustrative calculation excludes detection overhead and queueing.

Backend-only PRs can have a different outcome. Skipping a one-minute frontend job saves compute, but does little for feedback time if backend tests already take eight minutes in parallel. A repository dominated by backend changes will see smaller benefits from that particular rule.

For costs, record each job’s runner type and duration, then apply the provider’s billing rules. Combined runner-minutes do not translate directly into a bill when machine rates differ. Include the overhead of detection and final-check jobs in the comparison.

Uber’s engineers report both measures for their SubmitQueue optimisation: around 53% lower CI resource usage and 37% lower P95 waiting times across their Go, iOS, and Android monorepos. They changed speculative build scheduling and prioritisation. Those results show why both measures matter, but do not predict the savings from path-based routing in another repository.

Sharding and caching remain useful. Sharding can shorten a selected suite’s elapsed time while adding setup work across runners. Selective routing avoids starting that suite when the PR cannot affect it. Measure the combination against your own changes and runner costs.

Verify the rules before enabling skips

Test routing with disposable Git repositories. Cover changes to individual APIs, API dependencies, frontend code, shared inputs and documentation, plus deletions, cross-directory moves, unusual filenames and large diffs. Verify that every application change selects the full browser suite and known documentation-only changes select no application tests. Invalid commits and missing merge bases must fail detection. Test the final checks against failures, cancellation, missing flags, and unexpected skips.

Then exercise those categories in hosted CI and inspect the reported checks. A local classifier test cannot prove that workflow dependencies or branch protection behave correctly. During rollout, record what the detector would skip while continuing to run every suite. Investigate any failure in a suite it would have skipped before enabling the rule.

Keep scheduled dependency audits and broader post-merge verification independent of PR routing where they serve a different purpose. Even the full browser suite cannot establish compatibility for every API consumer. Shared contracts may need dedicated compatibility tests across affected APIs and the frontend.

Start with a rule for changes confined to one API, such as orders. Keep its complete suites and the full browser suite, then compare the proposed skips for unrelated APIs with full-suite runs. Verify documentation-only skips as a separate case. Enable the rules once those comparisons support them, then measure queue time, elapsed time and runner use before expanding selection to more APIs.

all posts →