Article Image

Modern trunk-based development

13th October 2022 ・ By Marcelo Sousa

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:

  1. TBD avoids branches which save developers’s time from merge hell situations;
  2. 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.

What changed?

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:

  1. Execute a set of automated tasks, e.g. build, tests, code analysis;
  2. Code review;
  3. 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 App.

To get Reviewpad in your repository you just need two steps:

  1. Add the reviewpad.yml file that specifies the pull request rules and automation workflows. This file is read by the action to understand which actions to perform.

If you install the GitHub app in a single repository, Reviewpad will automatically open a pull request with a starter configuration with checks and automations for pull request best practices.

For more details, check the installation tutorial.

The pull request workflows are specified in YAML. Reviewpad uses a domain specific language (DSL) to quickly access contextual information in the pull request and perform actions.

Consider the following configuration:

4 - name: label-pull-request-with-size
5 if:
6 - rule: '$size() <= 50'
7 extra-actions:
8 - '$addLabel("small")'
9 - rule: '$size() > 50 && $size() <= 150'
10 extra-actions:
11 - '$addLabel("medium")'
12 - rule: '$size() > 150'
13 extra-actions:
14 - '$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:

  1. Changes to Markdown files should be automatically merged; additionally, such pull requests shouldn't waste resources such as running the CI process;
  2. 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.

The implementation

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:

1## Description
3... for the entire file, please follow the link above :)
5## Code review and merge strategy (ship/show/ask)
7<!-- - [ ] Ship: this pull request can be automatically merged and does not require code review -->
8<!-- - [ ] Show: this pull request can be auto-merged and code review should be done post merge -->
9<!-- - [ ] 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:

  1. reviewpad.yml that runs Reviewpad;
  2. pull_request_build.yml that runs the CI with a job called pr-build.

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 marcelosousa and ferreiratiago.

2 - name: owners
3 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 $contains and $description.

2 ...
3 - name: ship-mode
4 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 Ship.

2 ...
3 - name: ship-authored-by-owners
4 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 .md extensions.

2 ...
3 - name: changes-are-in-markdown
4 spec: '$hasFileExtensions([".md"])'

Finally, we define the rule ship-markdown-changes that chains the previous rules and if true, should trigger the merge action.

2 ...
3 - name: ship-markdown-changes
4 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:

  1. Trigger the CI action if the changes are sensitive;
  2. 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 changes-are-in-markdown.

2 ...
3 - name: changes-should-be-built
4 spec: '!$rule("changes-are-in-markdown")'

We then combine this rule to define a Reviewpad workflow that adds a special label run-build.

2 - name: add-label-for-build
3 if:
4 - rule: changes-should-be-built
5 then:
6 - '$addLabel("run-build")'

The event of adding this label triggers the CI run in pull_request_build.yml with a job called pr-build.

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.

2 ...
3 - name: ci-is-green
4 spec: '$workflowStatus("pr-build") == "success"'

Finally, we can combine the rules to define the second auto-merge criteria:

2 ...
3 - name: auto-merge-authored-by-owners-with-ship-and-green-ci
4 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.

2 ...
3 - name: auto-merge-owner-pull-requests
4 if:
5 - rule: auto-merge-authored-by-owners-with-ship-and-green-ci
6 - rule: ship-markdown-changes
7 then:
8 - '$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, Reviewpad 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.

PS. Please give us feedback on Discord and support our project on GitHub with a star!

Try Reviewpad
Disrupting how software developers collaborate