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.ymldoes 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

Code. Gym. Music.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store