Branch Protection and Required Status Checks
So far, we have added several security measures: limited permissions, pinned actions, OIDC roles, and Dependabot. These protect our workflows and AWS access. But CI alone is not always enough. For example, someone could push directly to main. CI would run because of on: push, but the code is already in main before CI finishes — and CI may pass or fail. Do you want failed code in your main branch?
Branch protection prevents this. It can block direct pushes to main and require CI checks to pass before a pull request can be merged. CI checks the code. Branch protection makes sure those checks cannot be skipped.
Why This Matters More Now Then It Would Have At The Start
If you’d set this up back when this course started, it would’ve mainly protected against a broken build reaching main. That’s still true, but the stakes have changed. Your repository’s main branch is now the thing that, on every push, assumes an AWS IAM role scoped to real infrastructure — the S3 bucket and ECR repository the real practicals from previous section. A change that reaches main without review or without CI passing isn’t just risking a broken deploy anymore; it’s risking a broken deploy with production AWS access, running as whatever OIDC-trusted workflow triggers next.
Branch protection is what makes the rest of this section’s guarantees actually hold. A tightly scoped IAM trust policy that only accepts pushes from main doesn’t help if anyone with push access can merge anything to main unreviewed.
Required status Checks: Tying Protection To Your Real CI
A required status check blocks merging a pull request until a specific named check — mapped to an actual job in your workflows — reports success. This only works when the names line up exactly, so it’s worth confirming what your own jobs are actually called before configuring anything.
Open a recent workflow run in your repository’s Actions tab and look at the job names shown there — these are the names branch protection will ask you to match. From the workflow you built with your test suite back in earlier sections, that’s likely something like test; from the linting step, maybe lint. Use the names your own jobs actually report, not generic placeholders — if your test job is defined as jobs: run-tests: with name: Run test suite, that display name is what needs to match, not the job’s YAML key.
Setting It Up
In your repository, go to Settings → Branches → Branch protection rules → Add rule, and set the branch name pattern to main.
Enable Require status checks to pass before merging, then search for and select your actual job names from the list GitHub shows you — this list is populated from checks that have run recently, so it’s easiest to configure this after your workflows have run at least once on a recent PR. Also enable Require branches to be up to date before merging, so a PR can’t merge with checks that passed against a now-outdated version of main.
Alongside status checks, two more settings are worth enabling given what this repository now controls:
- Require a pull request before merging, with Require approvals set to at least 1. This stops a direct push to
mainentirely — every change has to go through a PR, whether or not that PR happens to also need CI. - Do not allow bypassing the above settings, so the rule applies to repository admins too, not just everyone else. It’s tempting to leave an escape hatch for yourself “just in case,” but an unenforced rule protects nothing at the exact moment it would matter most — under time pressure, during an incident, or from a compromised account.
flowchart TD
Push["Someone pushes a change"]
Push --> Direct{"Direct push to main?"}
Direct -->|Blocked| Reject["Rejected — PR required"]
Direct -->|Via PR| Checks{"Required status<br/>checks pass?"}
Checks -->|No| Wait["Merge blocked"]
Checks -->|Yes| Review{"Required approval<br/>given?"}
Review -->|No| Wait
Review -->|Yes| Merge["Merge allowed → main"]
Merge --> Deploy["Triggers OIDC-authenticated<br/>deploy workflow"]
What This Actually Buys You
Walk through what has to happen now for a change to reach main and, from there, touch real AWS infrastructure: someone opens a pull request, your test and lint jobs both have to report success against the current state of main, and at least one other person has to approve it. Only after all three hold does the merge go through — and only then does anything trigger the OIDC-authenticated deploy workflow.
Every other protection this section covered — least-privilege permissions, SHA-pinned actions, vetted third-party actions, an injection-safe handling of untrusted PR input, a narrowly scoped IAM role reachable only via short-lived OIDC tokens — assumes that what’s running is the code you actually intended to run, reviewed and verified before it got there. Branch protection is the setting that makes that assumption true.
Tip
If you’re working solo on this repository, “require approvals” can feel like friction with no one else to approve anything. It’s still worth requiring the pull request itself, even with approvals set to zero — that alone forces every change through CI before it can merge, which is most of the value. You can raise the approval count the moment a second person starts contributing.