Stop Running Every Check on Every Commit

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Full Pipeline on a README Change

A developer fixes a typo in the README. The pipeline starts: unit tests, integration tests, Docker build, security scan, staging deployment — all of it. 38 minutes later, the PR is green. The code that changed was documentation.

This is the default behavior of almost every CI system configured without path filtering. It's also wasteful in a way that compounds across dozens of commits per day. Worse, it conditions developers to see CI as a bureaucratic hurdle rather than a useful feedback loop — because a feedback loop that takes 38 minutes for a typo fix is not actually giving feedback, it's just burning time.

Path-Based Filtering: The Foundational Technique

Most CI platforms support conditional job execution based on which files changed in the commit. This single capability can eliminate a large fraction of unnecessary pipeline runs.

# GitHub Actions: only run backend tests when backend code changes
on:
  push:
    paths:
      - 'backend/**'
      - 'shared/**'
      - '.github/workflows/backend-ci.yml'

jobs:
  backend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew test
# Different workflow for frontend changes
on:
  push:
    paths:
      - 'frontend/**'
      - '.github/workflows/frontend-ci.yml'

jobs:
  frontend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

The paths filter means the backend pipeline doesn't run when only frontend/ changes. In a monorepo with multiple services, this is the difference between a 40-minute pipeline and a 6-minute pipeline for changes that touch one service.

Important caveat: the paths filter doesn't work well when there's real shared code that affects multiple services. If shared/ changes, you need to know which services depend on it and run those tests. This is where dependency graphs matter.

Dependency-Aware CI in Monorepos

For monorepos with multiple services or packages, the naive approach (run everything when anything in shared/ changes) is correct but expensive. The better approach is a dependency graph: each service declares what it depends on, and the CI system calculates the minimal set of services to rebuild and retest based on what actually changed.

Nx (for Node.js/TypeScript monorepos) and Gradle (for JVM monorepos) both handle this natively:

# Nx: only run tests for affected projects since last commit to main
npx nx affected --target=test --base=origin/main

# Gradle: only compile/test subprojects that depend on changed modules
./gradlew :service-a:test --parallel  # if service-a was determined to be affected

For Nx, affected uses your nx.json project graph to determine what changed. For Gradle, you can build a similar system using the gradle-enterprise plugin or a custom input fingerprinting strategy.

Stage-Level Gating Inside a Pipeline

Even within a single service pipeline, not every check needs to run on every commit. A useful pattern is tiered execution:

  • On every commit: compile, unit tests (fast, in-process)
  • On PR to main: + integration tests, lint, SAST scan
  • On merge to main: + Docker build, container scan, staging deploy
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - run: ./gradlew test

  integration-tests:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - run: ./gradlew integrationTest

  build-and-deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    needs: [unit-tests]
    steps:
      - run: ./gradlew bootJar
      - run: docker build -t myapp .
      - run: ./deploy.sh staging

The integration tests and Docker build don't run on every push to a feature branch. They run on PRs and on merge. This cuts the per-commit pipeline time significantly while keeping the pre-merge gate thorough.

The Risk: Missing a Signal

The obvious concern with selective execution is missing a failure. If you don't run the full suite on every commit, a broken commit might not be caught until it hits the PR gate.

This is acceptable if your branch lifetime is short (under a day) and your PR gate is thorough. If developers push once, wait for feedback, and push a fix — the delay in detection is a few hours, not a sprint. That's a reasonable trade for halving your pipeline runtime.

What's not acceptable: selective execution on the merge-to-main gate. Whatever you decide not to run on feature branch commits, run all of it before anything merges. The tiering described above maintains this invariant.

The Documentation Pipeline

Back to the README typo. A separate, minimal pipeline for documentation changes:

on:
  push:
    paths:
      - '**/*.md'
      - 'docs/**'

jobs:
  lint-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx markdownlint-cli '**/*.md'

Check markdown syntax. That's all. Two minutes. Done.

The goal is matching the check to the risk. Documentation changes carry documentation-level risk. Code changes carry code-level risk. Running the same pipeline for both is not cautious — it's indiscriminate.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Sinatra vs Rails — When I Reach for the Smaller Tool

Rails is not always the right Ruby web framework — Sinatra earns its place for a specific class of services where convention overhead is waste and explicit routing is an asset, not a liability.

Read more

Citadel and CME Group Pay Chicago's Backend Developers More Than Most Startups Can Afford

Chicago has world-class backend engineering talent. The financial firms that employ most of it have built compensation structures specifically designed to keep it.

Read more

Why Deleting Code Is One of the Most Underrated Engineering Skills

Every line of code that exists must be maintained, understood, and tested. Deleting code that no longer serves a purpose is not cleanup — it is removing a permanent tax from your team's velocity.

Read more

Why Backend Developers Often Carry the Most Responsibility in a Team

Backend developers rarely get the spotlight, but they often hold the threads that keep an entire system running. Their work affects performance, reliability, and scalability.

Read more