Running GitHub Actions Only for Changed Packages in a Monorepo
In a monorepo setup, you’re probably (like us at Microgamma) using Lerna to bump package versions, followed by some GitHub Actions to test, build, and deploy them.
One challenge we faced was: how can we run a job only if a specific package has changed?
Let’s walk through a simple GitHub workflow that shows 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 }}
The key piece is this step:
- name: Get changed packages
id: changed
run: |
CHANGED=$(npx lerna changed --json | jq 'map(.name)' -c)
echo ${CHANGED}
echo "changed=${CHANGED}" >> $GITHUB_OUTPUT
Here, all changed package names are collected into a changed output variable, which can then be used in later jobs:
should_run:
runs-on: buildjet-2vcpu-ubuntu-2204
needs: version
if: contains(needs.version.outputs.changed, 'my-package-a')
Why we store both the previous and next release tags
You’ll notice we capture both the current release tag (before bumping) and the next release tag (after bumping). This is crucial because of how Lerna’s run command works.
By default, lerna run test executes the test script in every package that has changed since the latest tag. But after running lerna version, a new tag is created, which means the “latest tag” changes — and that breaks the logic for detecting what really changed.
To solve this:
- We store the tag before bumping (so we can compare changes against it).
- We store the new tag (so subsequent jobs can check it out).
For example:
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
This job runs only if a new tag was actually created.
From there, we can run builds and deploys only for packages changed since the previous release:
- 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 }}
Why not just use lerna run?
Technically, we could just use lerna run ... and it would skip unchanged packages. But to even get to that point, the workflow still needs to:
Run the job
Checkout the repo
Install dependencies
…which wastes build time if no relevant packages actually changed.
By filtering earlier in the workflow, we avoid unnecessary jobs altogether.
Hopefully this helps anyone else running into the same issue we did! 🚀
Top comments (0)