GitHub Actions and the MonoRepo (or multi-module)
Mono Mono on the wall, who’s the largest of them all!
tl:dr https://github.com/peavers/million-dollar-idea
We’ve all heard the rumours and the sayings that monolithic repositories are the devil and to be avoided at all costs. One often-used argument is the pain in the CI/CD process when you make a change to one module the entire stack has to go unless you spend hours fiddling around with build scripts.
Quick note: I’m using a repository with Java and Typescript but this would be equally the same if you just had a single language but multimodule project.
That doesn’t have to be the case
Perhaps you're building a Springboot service that hosts its own Angular UI. This is how I build out just about all of my personal million-dollar ideas that I never get around to finishing. The folder pattern I just about always use looks like this:
million-dollar-idea
├── README.md
├── million-dollar-idea-core
├── million-dollar-idea-ui
Where the million-dollar-idea-core
is my Springboot project with all the Java goodness, and million-dollar-idea-ui
is typically an Angular Typescript project.
Problem
If I make a change to my million-dollar-idea-ui
the project, I’m going to end up building everything, including my Java source code. This is slow, and if I have specific needs in my UI workflow it's can be hard to inject those in (Think different Sonar profiles/projects for example).
The obvious way to solve this is to create two repositories, but then I’d need to publish my million-dollar-idea-ui
somewhere so I can use it as a dependency in my million-dollar-idea-core
. Annoying.
A Solution
It’s a lot easier than you might think. If we start with the .github
folder structure:
.github
├── dependabot.yml
└── workflows
├── build-java.yml
├── build-typescript.yml
├── release.yml
└── tag.yml
This should look similar to most, build-java.yml
does exactly what you think it might do. It builds the Java project with either Gradle or Maven whenever you commit to master or develop, and likewise with build-typescript.yml
but it does the NPM build.
We can tell GitHub Actions to only run if the changes didn’t occur in a specific path using paths-ignore
the option:
name: Build Java
on:
push:
branches:
- master
- develop
paths-ignore:
- "million-dollar-idea-ui/**"
And do a very similar thing in build-typescript.yml
action
name: Build Typescript
on:
push:
branches:
- master
- develop
paths:
- "million-dollar-idea-ui/**"
However this time we’re saying to only trigger changes that happen inside the million-dollar-idea-ui
directory.
What we have now is two GitHub Actions, each will only run if changes are made to the Java or Typescript codebases.
Getting ready for a release
We want to tag our project, but only on the condition that the Java build is successful. Since that is what is actually deployed and the UI is just a dependency on it.
Inside the tag.yml
file we start with:
name: Tag
on:
workflow_run:
workflows: ["Build Java"]
branches: [master]
types:
- completed
Note the workflows: ["Build Java"]
is the same name we gave to our Java GitHub Action. We’re saying only run the Tag action when that specific workflow completes.
One thing to note here is this will run no matter the outcome of the build, so if the build fails we will still be tagging a new version. That's not ideal so we can fix it with a simple if
statement
[...]
jobs:
tag:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
[...]
Now we’re only going to tag our repository with a new version if the Java Build runs and is successful.
Onto the release
As you guessed it, almost identical setup as the tag Action, however, we wait for the successful tag before running, in my case, I’m using Jib to push to Docker.
name: Release
on:
workflow_run:
workflows: ["Tag"]
branches: [master]
types:
- completed
jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Set up JDK 15
uses: actions/setup-java@v1
with:
java-version: 15
- name: Release
run: ./gradlew clean jib
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
Congratulations you’ve built out a nice workflow for the MonoRepo.
For complete examples checkout a sample responsory million-dollar-idea on GitHub