GitHub branch protection rules can feel like a gatekeeper, but they’re actually a powerful tool for ensuring your codebase stays healthy and your team collaborates effectively.
Let’s see this in action. Imagine a simple Python project. We want to make sure no one merges code that breaks the build or fails basic linting.
Here’s a workflow in GitHub Actions that runs flake8 for linting and pytest for tests:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
- name: Lint with flake8
run: |
flake8 .
- name: Test with pytest
run: |
pytest
Now, to enforce this, we go to your GitHub repository’s "Settings" > "Branches." We’ll add a rule for the main branch.
First, we’ll "Require status checks to pass before merging." This is the core of enforcing CI. We’ll search for the name of our workflow, "CI," and select it. This means a pull request targeting main must have a successful "CI" check from GitHub Actions before it can be merged.
But what if someone tries to merge code that doesn’t have a pull request? Or what if they have write access and bypass the checks? That’s where "Require branches to be up to date before merging" and "Require linear history" come in.
"Require branches to be up to date" ensures that the pull request branch has the latest changes from the target branch (main in this case). This prevents merge conflicts and ensures the code is tested against the most current version of the codebase. You’ll see a button to "Update branch" on the PR if it’s out of date.
"Require linear history" is a bit more subtle. It prevents merge commits from cluttering your history. Instead, it forces developers to use git rebase to integrate changes from main into their feature branch before merging. This keeps the commit history clean and easier to follow, making it simpler to pinpoint when a bug was introduced.
We can also "Require pull request reviews before merging." This is crucial for human oversight. You can specify the number of required approving reviews. For a small team, one might suffice; for larger projects, two or three could be appropriate. This ensures that at least one other developer has looked at the code and signed off on it.
Finally, "Restrict who can push to matching branches" allows you to control who has direct write access. Often, you’ll want to restrict this to specific individuals or teams, forcing everyone else to use the pull request workflow.
The true power here is that these rules create a safety net. They don’t stop developers from trying to merge bad code, but they prevent it from ever landing in your main branch. This drastically reduces the chances of introducing bugs, breaking the build, or creating a messy commit history.
Most people know you can require status checks and reviews. What they often overlook is the "Require branches to be up to date before merging" rule’s interaction with "Require linear history." When both are enabled, GitHub will automatically present the "Update branch" button on a PR, and if you try to merge without updating, it will fail, pushing you towards a git rebase workflow to integrate main into your branch, thus maintaining that linear history.
Once you have these rules in place, the next challenge is managing the complexity of multiple protected branches for different environments (e.g., main for production, develop for staging).