DEV Community

Cover image for Scoped Branch Protection With GitHub And Monorepos
Tim
Tim

Posted on

Scoped Branch Protection With GitHub And Monorepos

Background

Before we dive in, here is a brief overview of the technologies we will be working with.

GitHub Branch Protection

GitHub has an awesome feature called branch protection, which allows repository owners
"to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging."

GitHub Branch Protection

This post discusses the last part of that blurb: the ability to require status checks to pass before code can be merged.

Required Checks

Branch protection rules are only available in public or paid repositories. They are not available in free-tier private repositories.

Monorepos

The monorepo pattern of code organization has really taken off in the past few years. Having all your applications and shared packages live in one repo has many benefits. Tools like Nx and Turborepo make things even better by adding features like caching and parallel task execution.

Typically, applications live in separate directories within a monorepo like this

.
|-- apps
|   |-- api
|   `-- react-app
`-- packages
    `-- shared-package
Enter fullscreen mode Exit fullscreen mode

For more about monorepos, see this video

Monorepos + Required Checks

Protected branches with required status checks do not play very nicely with monorepos. There are a few ways things can go wrong if you're not careful. Let me explain.

Let's say you have a monorepo with an API and React application, each with its own test suite. You enable branch protection on the master branch and add a required status check to ensure tests pass before code can be merged.

name: PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
jobs:
  pr-check:
  # Run tests
Enter fullscreen mode Exit fullscreen mode

Adding the required check

The action needs to run before it will appear as an option in this section.

You have a feature branch that you want to merge into the master branch. It only changes code inside the app/api directory. You open a PR on GitHub, and the check begins to run.

Do you see the problem here? You'd be forgiven for missing it. I sure did.

You made changes to the API code, so it makes sense for the API tests to run. However, the check runs all the tests, no matter what. That means you not only have to wait for the API tests to run before you can merge, but you also need to wait for the React app tests to run. If the API tests finish first, you're stuck waiting for tests checking code you didn't change.

Waiting for the required check

This might not seem like a big deal, but if you have a large team opening many PRs across many apps and packages with many status checks, such as linting, testing, or formatting, this time can start to add up fast.

But you are a smart engineer, so you try to optimize the workflow by splitting the check into two: one for the API tests and the other for the React tests. You configure them to be triggered if a branch changes the code of the respective app. That way, when you only change the code inside the app/api directory, only the API tests will run, and vice versa with the React app.

name: API PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
    paths:
      - "apps/api/**"
jobs:
  api-check:
  # Run API tests
Enter fullscreen mode Exit fullscreen mode
name: React PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
    paths:
      - "apps/react/**"
jobs:
  react-check:
  # Run React tests
Enter fullscreen mode Exit fullscreen mode

Updating the required check

You update your branch protection rule with the two new checks and save. Mission accomplished.

The next day, you open a new PR with changes in apps/api directory. The API tests run and pass—fantastic! The React tests don't run—amazing! So much more efficient! Except... you can't merge. That tantalizing merge button is disabled. But why?

Unable to merge

The rule is not satisfied until all the required checks pass. To pass, a check needs to run. Since your changes only impact the apps/api directory, the React check did not run. Since the React check is required to pass to merge, the rule is not satisfied, and the merge is blocked.

This issue has caused me some grief since adopting monorepos, but no more!

Enter merge gatekeeper. What does it do? From the README

By placing Merge Gatekeeper to run for all PRs, it can check all other CI jobs that get kicked off, and ensure all the jobs are completed successfully. If there is any job that has failed, Merge Gatekeeper will fail as well. This allows merge protection based on Merge Gatekeeper, which can effectively ensure any CI failure will block merge. All you need is the Merge Gatekeeper as one of the PR based GitHub Action

In other words, it solves this exact problem. Let's see how to use it.

Create a new action that looks like this

name: Merge Protection
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
jobs:
  merge-gatekeeper:
    runs-on: ubuntu-latest
    permissions:
      checks: read
      statuses: read
    steps:
      - name: Run Merge Gatekeeper
        uses: upsidr/merge-gatekeeper@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

Enter fullscreen mode Exit fullscreen mode

Notice the action is not scoped to changes in a specific directory.

Next, update your branch protection rule so that this new check is the only one required.

Adding merge gatekeeper

Now, when you open a PR that changes only code in the apps/api directory, just the API test will run, and if they pass, you will be able to merge!

Checks pass

The same will be true if you make a change that impacts just the /react directory, or both.

And with that, you now have a protected branch with scoped status checks!

Top comments (0)