DEV Community

Davide Cavaliere
Davide Cavaliere

Posted on

GitHub Actions: run a job only if a package has changed

In a monorepo setup you are probably (like us at Microgamma) using Lerna to bump your packages and then run a some GitHub actions to test, build and deploy them.

One of the challenges we faced is: how can we run a job only if a package has changed?

Let's see a simple GitHub workflow to demonstrate our approach.

on:
  pull_request:
    branch: [master]

  workflow_dispatch:

jobs:
  version:
    runs-on: buildjet-2vcpu-ubuntu-2204

    outputs:
      tag: ${{ steps.released-tag.outputs.tag }}
      next: ${{ steps.next-tag.outputs.next }}
      changed: ${{ steps.changed.outputs.changed }}

    steps:

      - uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}
          repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
          fetch-depth: 0


      - uses: buildjet/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org/'

      - name: Install dependencies
        run: npm ci

      - id: released-tag
        name: Grab Latest Release Tag
        run: |
          tag=`git describe --tags --abbrev=0`
          echo "tag=$tag" >> $GITHUB_OUTPUT

      - name: Get changed packages
        id: changed
        run: |
          CHANGED=$(npx lerna changed --json | jq 'map(.name)' -c)
          echo ${CHANGED}
          echo "changed=${CHANGED}" >> $GITHUB_OUTPUT

      - name: Lint
        run: npm run lint:ci

      - name: Version
        run: |
          npm run version:ci

      - id: next-tag
        name: Grab Next Tag
        run: |
          tag=`git describe --tags --abbrev=0`
          echo "next=$tag" >> $GITHUB_OUTPUT

      - name: Check variable
        if: contains(env.changed, 'musicbox-web')
        run: echo ${{ env.changed }}


  should_run:
    runs-on: buildjet-2vcpu-ubuntu-2204
    needs: version
    if: contains(needs.version.outputs.changed, 'my-package-a')

    steps:
      - name: test
        run: echo ${{ needs.version.outputs.changed }}

  should_not_run:
    runs-on: buildjet-2vcpu-ubuntu-2204
    needs: version
    if: contains(needs.version.outputs.changed, 'my-package-b')

    steps:
      - name: test
        run: echo ${{ needs.version.outputs.changed }}

Enter fullscreen mode Exit fullscreen mode

The relevant parts here are:

      - name: Get changed packages
        id: changed
        run: |
          CHANGED=$(npx lerna changed --json | jq 'map(.name)' -c)
          echo ${CHANGED}
          echo "changed=${CHANGED}" >> $GITHUB_OUTPUT
Enter fullscreen mode Exit fullscreen mode

This will store all changed packages names in changed output variable that can be later used in another job as in:

  should_run:
    runs-on: buildjet-2vcpu-ubuntu-2204
    needs: version
    if: contains(needs.version.outputs.changed, 'my-package-a')
Enter fullscreen mode Exit fullscreen mode

Another interesting point on the above configuration is the reason why we capture the currently released tag (i.e.: before bumping) and the next releasing tag (i.e.: after bumping).

Before explaining this we need to look on how we get npm scripts to run only in the changed packages.

We leverage lerna run command which runs a given command only in the packages that have changed since a certain "thing". Lerna has some automatic way to understand what the "thing" is so usually just running lerna run test for example will run npm run test on each package that has the test script in their package.json and it is changed since latest tag.

This however doesn't work if we run the same after running lerna version because a new tag has been pushed.

So the "version" job bump changed packages and push a new tag back on the repo. This is the reason why we need to capture the new tag: in order to check that out in the subsequent jobs.
We do that with something like:

  Build_UI:
    runs-on: buildjet-2vcpu-ubuntu-2204
    needs: Version

    if: needs.Version.outputs.next != needs.Version.outputs.tag

    steps:

      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.Version.outputs.next }}
          repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
          fetch-depth: 0
Enter fullscreen mode Exit fullscreen mode

Also notice that this job only run if a new tag has actually been created.

Then we'll need to tell lerna to run, for example, build for all packages changed since previous release.

This is the reason for capturing the previous released tag.

So the rest of the workflow will look something like the following:

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: |
          npm run build:ci -- --since ${{ needs.Version.outputs.tag }}

      - name: Deploy
        run: |
          npm run deploy:ci -- --since ${{ needs.Version.outputs.tag }}
Enter fullscreen mode Exit fullscreen mode

Please note that we could just leverage lerna run ... command for that would not run in packages that didn't change. However in order to do that we still need to run the job, checkout the source code and install all packages consuming our precious build times for nothing.

Hope this can help somebody that like us is running into the same issues.

Top comments (0)