My Personal Monorepo & Pipeline

A monorepo is a software development strategy where code for many projects is stored in the same repository. The code doesn't necessarily have to be related.

Okay, but why use a monorepo? Gathering all of my personal projects into a single repository makes it easier for me to manage and maintain the code. One repository is easier to deal with than a dozen. I can develop a common interface for building and deploying projects in the monorepo. One build tool to rule them all. Versioning is not a concern for me here, but if it were, we also get the advantage of atomic commits.

Another advantage of the monorepo to consider would be the ease of re-using code between projects. For example, you could have a docker image that acts as the base of several projects. That base image is just another project managed in the monorepo, but that other projects in the monorepo are themselves built upon. Or, maybe you have shared 3rd party dependencies that you could simply download once and the other projects would then consume. Simpler dependency management is pretty great!

So what is my monorepo's directory structure?

/bin
/build
/config
/infrastructure
/src
  /lunchboxradio
  /roylart
  /roycom
  /matomo

Let's break this down.

/bin
Contains utilities for working with the monorepo (eg: setup and teardown utilities).
/build
This contains code related to building and deploying projects in the monorepo, the common cli.
/config
Global configuration for the monorepo (eg: env files for docker-compose could go here)
/infrastructure
Code related to building and provisioning infrastructure. Think terraform and ansible.
/src
Home to the individual projects

It's great to have everything under one roof, but for me the real power in the monorepo is being able to write tooling to build, deploy, and develop each of the projects contained within. To build this system I have chosen Ruby and Rake. I like writing code with Ruby, it's very pleasant and fun, and Rake is a nice interface for writing the sort of tool I am thinking of. You could do this with any tool and language you prefer!

The glue for my monorepo is this build system. Build system is expressed as Rake tasks. Very simply it works like this: I load project data from a file called build.yml then I generate build & deploy tasks for each project from that data. There is a builder class for each type of project that has methods for building and deploying the project.

build.yml example:

all:
  startable: false

roylart: start_by_default: true build_by_default: true after_build: - “cmd to run after building” before_build: - “cmd to run before building” build_type: capistrano tags: - wordpress

roycom: build_type: middleman

lbr: build_type: middleman

matomo: build_type: capistrano

The generated rake tasks look like:

$ rake -T
rake buildsystem:build                      # Build everything
rake buildsystem:deploy                     # Deploy everything
rake buildsystem:lbr:build                  # Build the lbr project
rake buildsystem:lbr:deploy                 # Deploy the lbr project
rake buildsystem:matomo:build               # Build the matomo project
rake buildsystem:matomo:deploy              # Deploy the matomo project
rake buildsystem:roycom:build               # Build the roycom project
rake buildsystem:roycom:deploy              # Deploy the roycom project
rake buildsystem:roylart:build              # Build the roylart project
rake buildsystem:roylart:deploy             # Deploy the roylart project

Adding a new project is easy: add the src files, add the project to build.yml, incldue a builder class if it does not already exist.

Now to build and deploy I simply run:

rake buildsystem:roycom:build buildsystem:roycom:deploy

I run a more complex version of this on a more important monorepo that builds docker images so there are additional tasks that tag and push images to a private docker repository with support for multiple docker clusters and environments. But I could also expand this to tar up a build and "tag" it with a version number, then "push" to an s3 bucket, where then deploy is to pull that tar onto some servers someplace. Really, it's a super flexible system that allows me to do anything I want.

The builder class looks something like:

module BuildSystem
  class Capistrano < Base
    def build
      BuildSystem.logger.info "Bundle Install #{@params[:service_name]}"
      BuildSystem.system! "cd src/#{@params[:service_name]}; bundle install"
    end
def deploy
  BuildSystem.logger.info "Deploying #{@params[:service_name]}"
  BuildSystem.system! "cd src/#{@params[:service_name]}; #{@build_args.join(' ') if @build_args} bundle exec cap #{BuildSystem.branch} deploy"
end

end end

With all of this in place it's now trivial to hook up to something like CircleCI, or GitHub actions, or Gitlab to build and deploy when you commit your changes, it's just running the commands you have designed.

So that's my monorepo in a nutshell. I think this is a super flexible system and a pretty decent way to manage a bunch of projects.