Our Journey to Monorepo
In this blog post, I’ll share the story of our journey to using a monorepo. It’s been a rocky road that’s demanded all the nicepeople have had to give, and, for most of them, led to a radical change in their daily work styles.
Let’s set the scene: it was 2021, the Corona pandemic had us in its grip, and our developers started their days by opening a project from one or more of our forty-plus shops. Each of these shops used a separate Git repository with three integrated submodules.
One module contained the configuration for all our shops and companies, and another module contained the shared source code, also known as “library source code”. This so-called “library source code” is an abstract implementation, because the equivalent, mostly empty, shop class derived from the library is always instantiated. This makes it possible to change the programme flow for individual shops by adding source code at the application level. The third module contains all shared images, JavaScript and CSS files.
The development flow usually began like this: starting from the team’s sprint branch, an approval branch was created via Jenkins and then pushed back after the work was finished. At the same time, a code review was created in Upsource and assigned to a team member. The unit tests were run on Jenkins overnight, and the disastrous results were presented the next day. As if that wasn’t enough, a Quality Assurance Manager came in and took the new masterpiece completely apart. If the final product survived all of these steps, the branch was merged back into the team branch via Jenkins, then the changes were merged back into Develop at the end of the sprint. Conflicts were almost – literally – pre-programmed into the two-week sprints.
Thankfully, everything is different now!
After about four months of prepping our system for the monorepo, we reserved an entire sprint to carry out the change across all our teams and eliminate any problems. We changed the entry points, did a deep clean and replaced application-level overrides with dependency injections. Parallel to our clean-up work, we wrote an importer for the source code from all of our shops so that the code could automatically transfer to the new repository. We needed eleven iterations until all the files were in the right place.
The DevOps team was also challenged by the change. First, they changed to Gitlab for source code management. Then, step by step, processes migrated from Jenkins to Gitlab Runner. A large part of the existing source code used for building our shops for the staging servers and the live system was rewritten and integrated directly into the main project as a CLI module. Using Gitlab Runner and switching to merge requests enabled a meaningful QA pipeline for the first time, using phpstan, phpcs, phplint and unit tests. The system was designed in such a way that the merge request checks which files have been changed and then triggers the correct pipelines by creating labels. For example, if a PHP file is added to a shop, the pipeline recognises the file immediately and executes the unit tests for this shop.
We also changed the way we develop – changing from Git flow to a trunk-based model.
Changing to a trunk-based model eliminated the team branches. Now, the central starting point for each branch is always the main. The goal is, that no merge request lasts longer than one day and that the source code is merged directly back into the main each evening. Important new vocabulary terms for us are “dark launching” and “feature flags, referring to when you put code online in the background and release it at a given time. All this runs automatically via merge requests in Gitlab, i.e. if you push a change, a merge request is automatically created and the QA pipeline starts working. At the same time, only one more team member needs to be assigned for code review. The maximum QA level even includes a review by a software quality manager. For this review, the shop is automatically built on the staging server and thus made available for testing. Once all three parties, the automatic QA pipeline, the code reviewer and the quality assurance manager have accepted the merge, the source is merged back into the main.
Now, you’re probably wondering why we went this route. Quite simply, a trunk-based model of development makes our daily work much easier. Instead of 51 projects, a developer only has one; the different shops are simply started via a CLI. Conflicts between teams are limited to a maximum of one day, and each team immediately benefits from new source code from the other teams. The introduction of the mandatory QA pipeline is extremely important, as it means that untested code can no longer be merged into the main.
All in all, we’ve reduced the code from 362,400 files in over 50 repositories to 43,500 files in a single project. In doing so, we’ve been able to rapidly accelerate the release of new features. Plus, the structure of our projects is much simpler and the system is ready to grow with new teams and shops.
Stay Nice!