Scoped Branch Protection With GitHub And Monorepos
Tags: github, devops, tutorial, monorepo
Published: 2024-03-21 01:23:08

## 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](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches), which allows repository owners 
"*to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging.*" 

![GitHub Branch Protection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ql3v4zauz14wvk60dc8k.png)


 This post discusses the last part of that blurb: the ability to [require status checks to pass before code can be merged](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging).

![Required Checks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23x5fjietuxsrdbrg7gm.png)

>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](https://nx.dev/) and [Turborepo](https://turbo.build/repo) 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
```

>For more about monorepos, see [this video](https://www.youtube.com/watch?v=9iU_IE6vnJ8)

## 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.
```yml
name: PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
jobs:
  pr-check:
  # Run tests
```

![Adding the required check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3tjus7b9eyekcok6a5oa.png)

>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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/huu71hau4913utiinrk5.png)


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.

```yml
name: API PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
    paths:
      - "apps/api/**"
jobs:
  api-check:
  # Run API tests
```

```yml
name: React PR Check
on:
  pull_request:
    types: [opened, synchronize]
    branches:
      - "master"
    paths:
      - "apps/react/**"
jobs:
  react-check:
  # Run React tests
```

![Updating the required check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62girg8aqvwn1tahobih.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oddjdsjnu4vku9y6e57g.png)

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](https://github.com/upsidr/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
```yml
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 }}

```
>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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a0od6gdd4ac04x8ttnyn.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5p43bhjngwmoweuihlu.png)

>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!