Git and GitHub the Safe Way, Ch. 6: Protecting main
Part of Git and GitHub the Safe Way, a short book on git, GitHub, and the habits that keep a repository from hurting you.
Chapter 5 described a way of working: branch, pull request, review, merge. This chapter makes GitHub insist on it. Promise three, nothing changes on main by surprise, is a setting, not a resolution, and it takes ten minutes.
GitHub’s current tool for this is a ruleset. (Older guides say “branch protection rules”; those still exist, but rulesets are what GitHub builds on now, and they work the same way with a clearer screen.) The click path below follows GitHub’s documentation, and the rule names are as they appear on the book’s own practice repository, whose ruleset anyone can read at its public rules page: GitHub shows every visitor which rules protect a branch, which is itself a small act of transparency worth copying.
Why, when it is only you
Because the protection is against mistakes, not against people. A ruleset on main means you cannot push a half-finished change there at midnight, cannot force-push over last week by accident, cannot delete the branch with a typo, and cannot merge something the tests reject. It costs you one extra step per change, the pull request, and chapter 5 argued you want that step anyway, for the review of your own diff. Every team you ever join will have this switched on. Learning to work inside it now, alone, means it never slows you down later.
Creating the ruleset
On the repository’s page, click Settings. In the left sidebar, under “Code, planning, and automation”, click Rulesets, then New ruleset, then New branch ruleset. Then, top to bottom:
- Ruleset name: something like
protect main. - Enforcement status: change it from Disabled to Active. A disabled ruleset only records what would have been blocked, which is useful for a week on a busy team and useless alone.
- Bypass list: leave it empty. The temptation is to add yourself “just in case”; the whole value is that there is no just-in-case. If you ever truly need to bypass, you can edit the ruleset, do the thing, and put it back, and that friction is the feature.
- Target branches: click Add a target and choose the option that includes the default branch. That is
main. - Under the rules, turn on these, in this order of importance:
- Require a pull request before merging. Set the number of required approvals to 1 if anyone else works on the repository, 0 if it is only you (a pull request from yourself cannot be approved by yourself, and GitHub will not let you merge with 1 required). Also tick Dismiss stale pull request approvals when new commits are pushed, so an approval of version one does not carry over to version five.
- Block force pushes. Chapter 8’s rule one, enforced by the server.
- Restrict deletions. Nobody deletes
main, including you with a typo. - Require status checks to pass, once you have a check to require (next section). Tick Require branches to be up to date before merging, so the check ran against the code as it will actually land.
- Require signed commits, if chapter 2’s signing is set up on every machine you push from. Otherwise leave it for now; it refuses honest unsigned commits as firmly as dishonest ones.
- Require linear history pairs with “squash and merge” from chapter 5 and keeps
git loga straight line.
- Click Create.
Try it: push a commit straight to main from your terminal. This is what the practice repository said when I did:
git push
error: GH013: Repository rule violations found for refs/heads/main.
Review all repository rules at https://github.com/t0ul/git-and-github-the-safe-way/rules?ref=refs%2Fheads%2Fmain
- Changes must be made through a pull request.
- Required status check "scan" is expected.
To github.com:t0ul/git-and-github-the-safe-way.git
! [remote rejected] main -> main (push declined due to repository rule violations)
error: failed to push some refs to 'github.com:t0ul/git-and-github-the-safe-way.git'
That message is the whole chapter working. The same change went in minutes later the right way: a branch, pull request #1, the check, a squash merge.
Plans: GitHub’s plans page lists protected branches, required reviewers and code owners among what GitHub Pro adds for private repositories, so on a free personal account do this chapter on a public repository. Organization-wide rulesets, one policy for every repository, are a Team and Enterprise feature.
A check worth requiring
“Require status checks to pass” needs something that reports a status. The simplest is a GitHub Actions workflow that runs on every pull request. A file in the repository at .github/workflows/ci.yml is enough. This is the practice repository’s, which runs chapter 4’s scanner over the change (a project with code would run its tests here too; the shape is the same in any language):
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Scan the change for live credentials
uses: trufflesecurity/trufflehog@e76dff8a7e57d6df0c60ad8cbf5d5cba505a8fc9 # v3.99.2
with:
extra_args: --results=verified,unknown
Three things to copy. permissions: contents: read gives the workflow the least it needs; chapter 9 explains why the default should be read-only. The actions are pinned to a commit id with the version in a comment, chapter 9’s other rule, so nobody can move a tag under you. And on: pull_request means it runs on every pull request, so it has a result to report by the time the merge button is considered.
The job’s name, scan here, is what you type into the ruleset’s status-check list. On pull request #1 it ran in fifteen seconds:
gh pr checks
scan pass 15s https://github.com/t0ul/git-and-github-the-safe-way/actions/runs/39157226790/job/117762827790
One honest detail from that first run: I tried to merge a few seconds after opening the pull request, before the check had reported, and GitHub refused with “the base branch policy prohibits the merge.” That is the rule doing its job against the author’s impatience, and the merge went through as soon as the check turned green.
Code owners
A file at .github/CODEOWNERS names who must review changes to which paths:
* @ada
.github/ @ada @mo
docs/ @mo
Patterns like .gitignore, one owner or more per line, last matching line wins; GitHub reads the file from .github/, the repository root or docs/, in that order. Tick Require review from Code Owners inside the pull request rule and a change to .github/ cannot merge without one of its owners approving, however many other approvals it has. On a solo project it is a note to your future collaborators about who knows what; on a team it is how the workflow files and the deployment config get a second pair of eyes every time.
What this does not do
A ruleset protects main on GitHub. It does nothing for the copy on your laptop, where you can still make any mistake you like (chapter 8 is for those), and nothing for the quality of the review itself: a pull request approved in four seconds passes the rule and protects nothing. That is chapter 7.
Conclusion
One ruleset on the default branch: require a pull request, block force pushes, restrict deletions, require the check, and when you are ready, require signatures and linear history. A ten-line workflow to give the check something to say. Code owners for the files that matter most. Done once, it turns chapter 5’s good habit into the only way the repository accepts change.