Monorepo Workflows#
A monorepo (short for monolithic repository) is a software development strategy where code for many applications, which may or may not be mutually dependent, is stored in the same version-controlled repository. This guide shows you how to optimize your Semaphore workflows for monorepo projects.
Some advantages of the monorepo approach are:
- Ease of code reuse — it is easy to abstract shared behavior into common libraries.
- Simplified dependency management — third-party dependencies are easily shared.
- Atomic commits across multiple applications — you can refactor multiple applications at once with a single commit.
- Single source of truth — there’s only one version of each dependency.
- Unified CI/CD — a standardized process can build and deploy every application in the repository.
Semaphore comes with out-of-box support for monorepos and provides an example project for you to try.
Setting up a monorepo project#
Let's say you have a simple monorepo project that consists of:
- A web application located in the
/web-app/
directory. - An iOS client application located in the
/ios/
directory. - A separate docs web page located in the
/docs/
directory.
We can set up a Semaphore pipeline that builds and tests each one of these components.
The YAML for this pipeline is:
version: "v1.0"
name: Monorepo project
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
blocks:
# Run this block on changes in the web-app folder at the root of the repository
- name: Test WEB server
dependencies: []
run:
when: "change_in('/web-app/')"
task:
jobs:
- commands:
- checkout
- cd web-app
- make test
# Run this block on changes in the ios folder at the root of the repository
- name: Test iOS client
dependencies: []
run:
when: "change_in('/ios/')"
task:
agent:
machine:
type: a1-standard-4
os_image: macos-xcode12
jobs:
- commands:
- checkout
- cd ios
- make test
# Run this block on changes in the docs folder at the root of the repository
- name: Test docs page
dependencies: []
run:
when: "change_in('/docs/')"
task:
jobs:
- commands:
- checkout
- cd docs
- make test
# Run this block on changes in web-app or ios folders at the root of the repository
- name: Integration tests
dependencies: ["Test WEB server", "Test iOS client"]
run:
when: "change_in(['/web-app/', '/ios/'])"
task:
jobs:
- commands:
- checkout
- make integration-tests
With this setup, we run separate tests for each part of the system and integration tests once both web application and iOS client tests pass.
Change-based block execution#
You can set the criteria for running jobs within a block in the Skip/Run conditions section. The run property is evaluated on each workflow to decide if the block should be run or skipped.
When combined with the change_in
function, which checks whether there were
recent changes on a given path, it allows us to only run tests for those
parts of the project that are currently being worked on.
In the example below, this means that if we are only working on the iOS
client app within the /ios/
directory of the repository, the only blocks
that will be executed are Test iOS client
and Integration tests
.
Everything else will be skipped.
This can significantly reduce time and cost while still providing you with required feedback for the changes introduced into the monorepo.
The change_in
function checks for changed files in recent commits. The
commit range analyzed depends on whether you're working on master/main
or on a branch/pull request. For more details and ways in which this can
be modified please check the reference.
Pipeline file changes - default behavior
By default, when you change the pipeline file, Semaphore will evaluate
every change_in
expression as true
. That means that all blocks having
that expression will be run/skipped. You can avoid that by
excluding pipeline changes
from evaluation.
Set up automatic deployments for a monorepo project#
Here we will assume that you already have three pipelines for:
- Deploying the web app.
- Releasing the iOS client.
- Publishing documentation pages.
You can find examples for various kinds of deployments in the use cases section of our documentation. We will focus on auto-promoting the right pipelines using change detection. To achieve this, we need to introduce promotion conditions.
Ticking Enable automatic promotion brings up the conditions field.
Semaphore supports using change_in
in this field. You can combine it
with the branch
and result
properties to start a pipeline on a given
branch. E.g. for the web app:
To complete the example, this is how the iOS Release
and Publish
docs
pipelines should look:
The resulting YAML for the promotions is:
promotions:
# deploy web app when test pass on master branch and there are changes in the web-app folder
- name: Deploy Web Server
pipeline_file: web-prod.yml
auto_promote:
when: "branch = 'master' and result = 'passed' and change_in('/web-app/')"
# deploy web app when test pass on master branch and there are changes in the ios folder
- name: Release iOS client
pipeline_file: ios-prod.yml
auto_promote:
when: "branch = 'master' and result = 'passed' and change_in('/ios/')"
# deploy web app when test pass on master branch and there are changes in the docs folder
- name: Publish docs
pipeline_file: docs-prod.yml
auto_promote:
when: "branch = 'master' and result = 'passed' and change_in('/docs/')"
Each part of the system will be automatically deployed when the tests pass on
the master branch, but only if the push that initiated workflow contains changes in
the locations monitored by the change_in
function.
Additional examples of monorepo configuration#
When a directory changes#
change_in('/web-app/')
When a file changes#
change_in('../Gemfile.lock')
Changing the default branch from master to main#
change_in('/web-app/', {default_branch: 'main'})
Excluding changes in the pipeline file#
Note: If you change the pipeline file, Semaphore will consider change_in
as true. The following illustrates how to disable this behaviour.
change_in('/web-app/', {pipeline_file: 'ignore'})