Protected Branches & Rulesets
Branch protection rules are essential for maintaining code quality and preventing mistakes. Learn how to protect your main branches, require status checks, enforce linear history, and set up rulesets for your repository.
Protected branches are a security feature in GitHub that prevents developers from accidentally pushing directly to critical branches like main or master. Instead of allowing direct pushes, protection rules enforce that all changes must go through pull requests. This creates a natural checkpoint where code can be reviewed, tested, and discussed before it becomes part of the main codebase.
Without branch protection, it's easy for someone to accidentally push a broken commit directly to main, potentially breaking the build for everyone on the team. With protection enabled, you create a safety net. Even team members with write access cannot bypass the review process. This is especially important for production branches and long-lived integration branches where stability is critical.
Branch protection serves multiple critical purposes in a professional development workflow. First and foremost, it prevents mistakes. We've all had that moment where we accidentally pushed to the wrong branch or committed something that broke the build. Protected branches eliminate this risk entirely by blocking direct pushes entirely.
Beyond preventing accidents, branch protection enforces a consistent code review process. When every change to main must go through a pull request, you guarantee that no code enters production without being reviewed by at least one other developer. This dramatically improves code quality, spreads knowledge across the team, and catches bugs before they reach users.
Protected branches also integrate with your CI/CD pipeline. You can require that all automated tests pass before a pull request can be merged. This ensures that broken code never reaches your main branch, keeping your repository in a constantly deployable state. For teams practicing continuous deployment, this is essential.
GitHub offers several types of branch protection rules that you can enable individually. The most commonly used rule is Require a pull request before merging. This prevents direct pushes and requires all changes to be submitted via pull request. You can also require that pull requests have at least one approving review before they can be merged, and you can optionally dismiss approvals when new commits are pushed.
Require status checks is another critical rule. This forces your CI/CD pipeline to pass before merging. You can specify which status checks must succeed—for example, unit tests, linting, integration tests, or security scanning. If any of these checks fail, the merge button stays disabled until the issues are fixed.
Require linear history prevents merge commits and forces a clean, linear commit history. This is particularly useful for teams that prefer rebasing over merging. When enabled, pull requests must be rebased onto the target branch rather than merged, eliminating merge commits and creating a cleaner history.
Restrict who can push to matching branches allows you to limit which users or teams have the ability to push to protected branches. Even with protection enabled, you can still allow specific administrators or automation accounts to have bypass permissions when necessary.
# Example status checks that teams often require:
- CI / tests (passing all unit and integration tests)
- CodeQL / security analysis (no critical vulnerabilities)
- Lint (code style compliance)
- Build (application compiles successfully)
Setting up branch protection in GitHub is straightforward. Navigate to your repository, then click on Settings > Branches. Under "Branch protection rules," click Add rule. In the "Branch name pattern" field, enter the name of the branch you want to protect. For the main branch, you would typically enter main or master. You can also use patterns like release/* to protect all release branches.
Next, check the protection rules you want to enable. At minimum, most teams enable "Require a pull request before merging" and "Require status checks to pass before merging." If you have automated tests set up, you'll need to specify which status checks are required. GitHub will show you a list of available status checks from your repository's actions.
Once you've configured your rules, click Create. From that moment forward, the branch is protected. Anyone trying to push directly will receive an error message. Pull requests targeting that branch will display a checklist of required reviews and status checks, and the merge button will remain disabled until all conditions are satisfied.
main with strict rules, while protecting develop with slightly relaxed requirements. This gives you flexibility while maintaining safety where it matters most.
GitHub has introduced a new feature called Rulesets that extends and improves upon traditional branch protection. While branch protection rules apply only to specific branch names, rulesets can be configured with much more flexibility. You can create rules that apply to multiple branches, tags, or even entire repositories. Rulesets also allow for more granular control over who can bypass rules and under what conditions.
Rulesets can include multiple rules in a single configuration. For example, you might create a ruleset called "Production Protection" that applies to all branches matching main, release/*, and production/*. Within that ruleset, you can configure requirements for pull requests, status checks, and even restrict the types of files that can be changed in certain branches.
Rulesets also support bypass lists more flexibly than traditional protection. You can specify users, teams, or apps that are allowed to bypass specific rules. This is useful for deployment automation, where you might want a CI/CD bot to be able to push to production branches without requiring manual review, while still preventing developers from doing so.
Linear history is a Git feature that prevents merge commits and forces a clean, straightforward commit history. When you enable "Require linear history" on a protected branch, GitHub will reject any pull request that cannot be merged using a fast-forward strategy. This means that instead of creating a merge commit (which records the fact that two branches were merged), developers must rebase their feature branch onto the target branch.
Why would you want linear history? A linear commit history is easier to read and understand. Instead of a tangled web of merge commits, you see a single chronological line of changes. This makes it much easier to trace when a specific change was introduced, and it simplifies tasks like reverting changes or debugging with tools like git bisect.
Teams that adopt linear history typically use rebasing workflows. Instead of merging feature branches, they rebase onto main and then fast-forward. This eliminates the merge commits entirely. The trade-off is that rebasing rewrites commit history, which can be problematic for shared branches. For this reason, linear history is usually applied only to long-lived branches like main and develop.
When setting up branch protection, start with your main production branch. This should have the strictest rules: require pull requests, require at least one approving review, require all status checks to pass, and require linear history. This ensures that only reviewed, tested code reaches production.
Consider protecting your development branch as well, but perhaps with slightly relaxed rules. For example, you might require pull requests but not require linear history, or require status checks but allow the author to merge their own PR. This balances safety with development velocity.
Make sure your CI/CD pipeline is reliable before requiring status checks. If tests are flaky or slow, requiring them to pass will create frustration. Invest in making your tests fast and stable first. Also, consider requiring security scanning tools like CodeQL as part of your status checks to catch vulnerabilities automatically.
Finally, document your branch protection strategy. Make sure all team members understand why branches are protected and how the workflow works. When people understand the reasoning behind the rules, they're more likely to follow them enthusiastically rather than seeing them as obstacles.
release/*. This is incredibly useful for teams that follow a consistent naming convention for feature branches or release branches.Protect your branches, protect your code quality, and ship with confidence.