Logo

dev-resources.site

for different kinds of informations.

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

Published at
9/19/2024
Categories
cicd
monorepo
lerna
Author
davidecavaliere
Categories
3 categories in total
cicd
open
monorepo
open
lerna
open
Author
15 person written this
davidecavaliere
open
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.

monorepo Article's
30 articles in total
Favicon
Creating a scalable Monorepo for Vue - Workspaces
Favicon
Creating a scalable Monorepo for Vue - Intro
Favicon
Unlocking the Power of Modern Web Architecture: Monorepos, Micro-Frontends, and Vite 🚀✨
Favicon
Emerging Trends in Git: GitOps, Monorepos, Distributed Repositories, and AI Integration
Favicon
Using Node's built-in test runner with Turborepo
Favicon
Building a Scalable Monorepo with TurboRepo
Favicon
Cookiecutter for fast starting with polylith
Favicon
Monorepo vs Microservices: Finding the Perfect Fit for Your Project
Favicon
Nx + TypeORM + NestJS + Migrations
Favicon
How to Build a Changelog Feature in React for Your App
Favicon
Convert a ReactJS app from Vite to Nx
Favicon
How to deploy Google Cloud Functions with PNPM workspaces
Favicon
How CodeMirror v6 dev setup installs packages without a monorepo
Favicon
How CodeMirror v6 dev setup retrieves packages without a monorepo
Favicon
Nx 20: Exploring the new TS preset and TypeScript project references
Favicon
Simple hello world program using Bazel and Go lang
Favicon
A practical example of shared libraries in a monorepo
Favicon
🗂️ Monorepo vs. Polyrepo: Choosing the Right Strategy for Your Projects 🚀
Favicon
Turborepo vs Nx: Mana yang Terbaik untuk Monorepo?
Favicon
Turborepo vs Nx: Which Monorepo Tool is Right for You?
Favicon
Optimizing +200 Pipelines of a Monorepo
Favicon
GitHub Actions: run a job only if a package has changed
Favicon
Building a Solid Foundation: Bootstrapping with Turbo Repo
Favicon
Nestjs Workspaces to build Monorepo
Favicon
Installing EmberJS v2 addons from GitHub forks using PNPM
Favicon
Understanding Monorepo
Favicon
Build Containerized MERN App with Lerna Monorepo
Favicon
Advanced monorepo management with Turborepo 2.0
Favicon
Vite config reuse
Favicon
Monorepo VS Polyrepo

Featured ones: