Skip to content
Latchkey

How to Build Only Changed Services in a Monorepo in CI

A detect job emits the list of changed services as JSON, and a matrix build job builds only those images.

Use dorny/paths-filter (or a git diff) to compute which service directories changed, emit them as a JSON array, then consume it with matrix: ${{ fromJSON(...) }} so each build job handles exactly one changed service.

Steps

  • In a detect job, compute the changed service list and write JSON to $GITHUB_OUTPUT.
  • Expose it as a job output.
  • In the build job, set the matrix from fromJSON(needs.detect.outputs.services).

Workflow

.github/workflows/docker.yml
jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.f.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
      - id: f
        uses: dorny/paths-filter@v3
        with:
          filters: |
            api: 'services/api/**'
            web: 'services/web/**'
  build:
    needs: detect
    if: needs.detect.outputs.services != '[]'
    strategy:
      matrix:
        service: ${{ fromJSON(needs.detect.outputs.services) }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/build-push-action@v6
        with:
          context: services/${{ matrix.service }}
          push: false

Gotchas

  • Guard the build job with if: ... != '[]' so an empty change set produces zero jobs cleanly.
  • A shared base image change should rebuild every dependent service; add it as a path that maps to all services.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →