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 }}
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
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')
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
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 }}
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)