For the old guard software developers, trunk-based development is the way to go. For younger developers, used to GitHub pull requests, it is unlikely that they know what trunk-based means. This article shows how we use automation to combine TBD principles with pull requests and get the best of both worlds.
Trunk-based development (TBD) is a software development methodology where the developers continuously merge changes into a single long-lived branch.
Two benefits of TBD stand out:
- TBD avoids branches which save developers’s time from merge hell situations;
- TBD fosters small atomic commits which are essential to build loosely-coupled systems where a developer has the ability to change one part of the system without impacting the rest.
For engineering leaders, TBD seems a no brainer and Google’s State of DevOps Report has consistently associated TBD with a higher software delivery performance.
At the core, TBD fosters an high-trust and low-blame cultures which tend to have higher organizational performance.
However, one of the surprises of the 2022 State of DevOps Report is that trunk capabilities had a negative impact on software delivery performance.
According to the report, the number of respondents in the study with 16+ years of experience decrease from 40% to 13%.
Developer experience is a key factor to implement and benefit from TBD practices.
The report mentions that this difference is likely due to the additional practices required to successfully implement Trunk-based development. These additional practices can be resumed to a robust Continuous Integration (CI) mechanism that ensures that the trunk/main branch is never broken.
A different perspective
Over the past years, we have also seen a decline of interest in TBD when we interviewed developers with less than 10 years of experience. However, we observed that the main reason is not related with the lack of a robust CI but with the adoption of git-based version control systems like GitHub, GitLab and Bitbucket.
These version control systems naturally force developers to use feature branches and asynchronous collaboration via pull requests. There are many junior developers that never heard of TBD.
We also found that teams that rely heavily on feature branches and pull requests typically have strong CI mechanisms to ensure that the pull request merge does not break the long lived branches. In fact, GitHub Actions or GitLab Pipelines simplify the process to quickly get a basic automated CI system up and running.
Pull requests as a git merge blocking mechanism is necessary in low-trust environments found in open source projects where you have contributions from untrusted parties.
Teams practicing TBD understand that it doesn’t make sense to generalize this reality to a team of software developers where high-trust, low-blame culture is key.
The best of both worlds
We see pull requests as a useful mechanism to automate, document and audit three tasks:
- Execute a set of automated tasks, e.g. build, tests, code analysis;
- Code review;
- Git merge approval.
We believe the adoption of pull requests will increase in the future for security reasons. For example, a two-person reviewed requirement must be fulfilled for a team to reach level 4 of the Supply chain Levels for Software Artifacts standard.
We found that teams doing TBD find it hard to adopt pull requests because it creates a lot of friction in their processes.
Reviewpad started from the observation that not every pull request is the same.
This fits nicely for teams updating their TBD practices to use pull requests.
For example, a pull request that introduces a database migration is very different from a pull request that performs a minor refactoring of the code. In the first case, it makes sense to have a more in-depth code review and testing procedures to ensure the correctness of the migration.
At the moment, you can enforce pull request workflows to deal with this separation of concerns using the GitHub Reviewpad Action.
To install it in your repository you just need to add two files:
- The GitHub Action YAML file that lives in the
- The Reviewpad YAML file that specifies the pull request rules and automation workflows. This file is read by the action to understand which actions to perform. By default, the action assumes that this file is called
reviewpad.ymland lives in the root of the repository.
The pull request workflows are specified in YAML. The major advantage with respect to other GitHub Actions is that it comes with a domain specific language (DSL) to quickly access contextual information in the pull request and perform actions.
Consider the following configuration:
workflows: - name: label-pull-request-with-size if: - rule: '$size() < 50' extra-actions: - '$addLabel("small")' - rule: '$size() >= 50 && $size() < 150' extra-actions: - '$addLabel("medium")' - rule: '$size() >= 150' extra-actions: - '$addLabel("large")'
It specifies a Reviewpad workflow to automatically add a label to the pull request based on the size of changes (measured in the number of lines added and deleted).
We currently have over 65 built-ins that allow you to manage labels, comments, reviews and merge pull requests.
Reviewpad’s trunk-based development
Based on TBD practices, we use Reviewpad to auto-merge pull requests in our main open-source repository reviewpad/reviewpad.
For us, the first step was to define which scenarios it makes sense to auto-merge pull requests.
Since the repository is public, we need to restrict auto-merges to pull requests authored by owners of the project.
Additionally, to enable this auto-merge feature, the pull requests should be directly flagged by the owners as
Ship pull requests. The only requirement was that this flag mechanism should be automatic when opening the pull request.
Finally, we established two criteria for auto-merges
Ship pull requests opened by owners:
- Changes to Markdown files should be automatically merged; additionally, such pull requests shouldn't waste resources such as running the CI process;
- Other changes should be automatically merged if the CI process is successful.
Regardless of the author of the changes, we also wanted to avoid running the CI process unnecessarily. So, pull requests that only modify Markdown files should not trigger the CI process.
In this section, we show how we get Reviewpad to automate the merges.
The first step was to find a flexible mechanism for developers to open a pull request and flag them as
Ship. We heavily rely on the GitHub CLI to open pull requests and we open them as fast as possible.
So we decided to use pull request templates to allow developers to select which approval/merge mode they want. This is part of our organization pull request template:
## Description ... for the entire file, please follow the link above :) ## Code review and merge strategy (ship/show/ask) - [ ] Ship: this pull request can be automatically merged and does not require code review - [ ] Show: this pull request can be auto-merged and code review should be done post merge - [ ] Ask: this pull request requires a code review before merge
In the final section of the template, authors of pull request select this mode.
As soon as the pull request is opened, the developer shouldn't have to do anything else.
We achieved this with two GitHub Actions:
- reviewpad.yml that runs Reviewpad;
pull_request_build.yml that runs the CI with a job called
As soon as a pull request is created, the Reviewpad action will be triggered and reads the configuration file.
We now highlight the relevant sections of the specification.
Check for ship pull requests authored by owners
The first step is to specify a rule that will be true if the pull request has been selected as
Ship and is authored by an owner.
We start by defining, with the Reviewpad DSL, the group
owners as the list of GitHub usernames
groups: - name: owners spec: '["marcelosousa", "ferreiratiago"]'
Then, we define a rule that checks if the pull request contains in the description the section of the
Ship mode. For this we can use the Reviewpad built-ins
rules: ... - name: ship-mode spec: '$contains($description(), "[x] Ship:")'
We can use this rule to define the rule
ship-authored-by-owners that checks if the pull request is both authored by an owner and was flagged with
rules: ... - name: ship-authored-by-owners spec: '$rule("ship-mode") && $isElementOf($author(), $group("owners"))'
Check for pull requests that only modify Markdown files
The first auto-merge criteria is to auto-merge pull requests that only modify Markdown files.
To do this, we define a rule
changes-are-in-markdown that checks if the files modified only contain
rules: ... - name: changes-are-in-markdown spec: '$hasFileExtensions([".md"])'
Finally, we define the rule
ship-markdown-changes that chains the previous rules and if true, should trigger the merge action.
rules: ... - name: ship-markdown-changes spec: '$rule("ship-authored-by-owners") && $rule("changes-are-in-markdown")'
Check for pull requests that have successful CI
The other criteria for the auto-merge involves the following steps:
- Trigger the CI action if the changes are sensitive;
- Check if the CI action was successful.
For the first step, we conservatively trigger the CI process if the changes involve other file extensions. We define it by simply negating the result of the rule
rules: ... - name: changes-should-be-built spec: '!$rule("changes-are-in-markdown")'
We then combine this rule to define a Reviewpad workflow that adds a special label
workflows: - name: add-label-for-build if: - rule: changes-should-be-built then: - '$addLabel("run-build")'
The event of adding this label triggers the CI run in pull_request_build.yml with a job called
We leverage the fact that you can trigger GitHub Actions based on other GitHub Action workflows. This allows us to trigger Reviewpad once the build finishes.
Reviewpad comes out of the box with built-in functions that allow us to check the result of workflow jobs. So, we defined a rule
ci-is-green that checks if the
pr-build job succeded.
rules: ... - name: ci-is-green spec: '$workflowStatus("pr-build") == "success"'
Finally, we can combine the rules to define the second auto-merge criteria:
rules: ... - name: auto-merge-authored-by-owners-with-ship-and-green-ci spec: '$rule("ship-authored-by-owners") && $rule("ci-is-green")'
Auto-merge pull requests
Once we have the rules that define the auto-merge criteria of interest, we just need to use them in a workflow that will trigger the
$merge built-in. This built-in receives the merge method - we decided to use
rebase to preserve the commits in the pull request.
workflows: ... - name: auto-merge-owner-pull-requests if: - rule: auto-merge-authored-by-owners-with-ship-and-green-ci - rule: ship-markdown-changes then: - '$merge("rebase")'
See it in action
When an owner opens a
Ship pull request, if the changes are only in Markdown files, the pull request will be automatically merged without wasting the resources to run the CI.
See the following pull request for an example.
If the changes are in other file extensions, Reviewpad will add the
run-build label and this label will trigger the CI. Once the CI job is completed, the Reviewpad action will run again and now validate the result of the CI.
This way we can automatically merge pull requests with the guarantee that the CI process was successful.
See the pull request for an example.
That's it! This is how we ensure that some pull requests are systematically merged without manual code reviews with the guarantee that the CI process is green.
If you are curious to see the whole process in action, check out our GitHub repository.