I was working on a mono-repo based micro-services project recently and I realised that the CI/CD pipelines were taking too much time. Upon a closer look I noticed that on every run, the pipelines were rebuilding all micro-services!

So here’s a simple pipeline that runs only the changed micro-services,

Project specifics

  • My project used Gradle as the over all build tool
  • All micro-services are located within the /services directory from the root

Bash script to find which service has changed

modified_services=$(
  git diff --dirstat=files $1 \
  | grep -E 'services/[^/]+/' \
  | cut -d'/' -f2 \
  | uniq \
  | tr '\n' ',' \
  | sed 's/,$//'
)
 
run_jobs=1
if [[ ! $modified_services ]]; then
  run_jobs=0
fi
 
json_string=$(echo $modified_services | sed 's/,/"},{"name":"/g; 1s/^/[{"name":"/; $s/$/"}]/')
 
echo "$json_string $run_jobs"

Workflow that uses the script

jobs:
  identify-modified-services:
    runs-on: [ 'ubuntu-latest' ]
    outputs:
      run_jobs: ${{ steps.identify_step.outputs.run_jobs }}
      modified_services: ${{ steps.identify_step.outputs.modified_services }}
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 2
 
      - name: Identify modified services
        id: identify_step
        run: |
          read previous_sha <<< $(git rev-parse HEAD~1)
          read services flag <<< $(.github/workflows/services_diff.sh $previous_sha )  
          echo "modified services: $services"
          
          echo "run_jobs=$flag" >> $GITHUB_OUTPUT
          echo "modified_services=$services" >> $GITHUB_OUTPUT 

Using the outputs

job2:
    needs: [ identify-modified-services ]
    if: needs.identify-modified-services.outputs.run_jobs == '1'
    steps:
      - env:
          services: ${{ fromJSON(needs.identify-modified-services.outputs.modified_services) }}
        run: echo "$services"

%%

Using the outputs in a matrix strategy

  pr:
    needs: [ identify-modified-services ]
    if: needs.identify-modified-services.outputs.run_jobs == '1'
    strategy:
      fail-fast: false
      matrix:
        services: ${{ fromJSON(needs.identify-modified-services.outputs.modified_services) }}
    uses: ./.github/workflows/_build.yml
    with:
      service-name: ${{ matrix.services.name }}

%%


Refs

  1. https://github.com/orgs/community/discussions/25669