Branch Protection - Forgejo Rules, Required Status Checks, and PR Workflows
Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 2: Workflow Governance
Prerequisites: forgejo-installation, woodpecker-first-pipeline, repository-setup
Time: 1-2 hours
Tags: forgejo, branch-protection, pull-request, status-checks, review, workflow
Summary
Make main trustworthy: configure Forgejo branch protection so protected branches only accept changes through reviewed PRs with passing Woodpecker pipelines. This article walks through the settings, the matching between CI checks and protection rules, and a PR workflow that keeps both humans and automation honest.
π― What You'll Learn
By the end of this article, you'll be able to:
- β Create and tune branch protection rules in Forgejo
- β Require Woodpecker status checks before merge
- β Run a review-based PR workflow without blocking solo maintainers
- β
Understand why direct pushes to
mainshould be rare exceptions
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
- Sources & Related
Context / Why This Matters
Your pipeline is only as good as what you let into it. If anyone (or any compromised account β see user-management and two-factor-authentication) can push straight to main, then every gate you built in security-scanning can be bypassed by simply not triggering it on the bad commit.
Branch protection closes that hole at the Forgejo layer: the rules live with the repository, apply to everyone including admins, and are enforced before code merges β independent of CI configuration.
Implementation / Core Content
Configuring protection in Forgejo
Repository β Settings β Branches β Add New Rule:
| Setting | Recommended value | Why |
|---|---|---|
| Branch name pattern | main (or glob release/*) |
Protect long-lived branches; leave feature branches free |
| Enable Push to Whitelist | enabled, list specific users or empty | Empty whitelist = nobody pushes directly, all changes via PR |
| Enable Merge Whitelist | enabled | Control who may click merge |
| Require approvals | 1 (homelab) / 2 (shared projects) | Human checkpoint per change |
| Dismiss stale approvals | enabled | A new push invalidates prior reviews |
| Require signed commits | optional | Pairs with commit signing if you use it |
| Block merge on outdated branch | enabled | Forces rebase/merge of main before merging |
| Required status checks | see below | The CI gate |
Required status checks: making CI mandatory
In the same rule, add required status checks matching your Woodpecker workflow names. Woodpecker reports statuses to Forgejo as <workflow-name> (from .woodpecker.yml) β for matrix workflows each leg appears separately (matrix-builds).
For this pipeline shape:
steps:
test:
image: node:22
commands:
- npm ci && npm test
Add test as a required check. Now "merge" stays grey until Woodpecker reports success for that exact commit SHA.
Critical detail: status checks are keyed to the commit. Force-pushing after a green run invalidates it β which is why Dismiss stale approvals + Block merge on outdated branch belong together with required checks.
Triggering pipelines on PRs
Ensure .woodpecker.yml runs on pull_request events, otherwise required checks stay forever "pending":
when:
event: [push, pull_request]
steps:
test:
image: node:22
commands:
- npm ci && npm test
secrets:
image: zricethezav/gitleaks:v8
commands:
- gitleaks detect --source . --redact --verbose
Verify the Forgejo β Woodpecker webhook wiring first β PR events travel over the same webhook as pushes configured during woodpecker-installation; broken webhooks are the top cause of never-ending pending checks (see webhooks).
Solo-maintainer mode
"Require approvals = 1" blocks a single-owner repo from merging its own PRs. Options:
- Set approvals to 0 but keep required status checks β CI gates everything, humans nothing.
- Keep approvals at 1 and grant a second account (or accept the friction) β appropriate where changes are risky.
- Use Forgejo's setting allowing admins to override merge whitelist for emergency fixes; audit those merges afterward.
Emergency bypass procedure
Even with strict protection you need a documented escape hatch for hotfixes:
- Open a PR against
mainanyway (protection applies to direct pushes, not PRs). - If CI infrastructure itself is down, an admin uses merge-whitelist override.
- Immediately file a follow-up issue documenting why; retro-run CI once it's back.
Never disable the protection rule itself β forgotten re-enables are how protection silently disappears.
Practical Examples
Example: homelab service repo baseline
Protection rule for main:
- Push whitelist: empty (no direct pushes)
- Merge whitelist: repo admins
- Approvals: 1, dismiss stale: on
- Block merge on outdated branch: on
- Required checks:
test,secrets,scan(matching step names from security-scanning)
Flow of a typical change:
git checkout -b fix/healthcheck-timeout
# ... edits ...
git push -u origin fix/healthcheck-timeout
# open PR in Forgejo UI
Expected sequence in the PR: three status checks appear within seconds (webhook fires), turn green after ~2 minutes, "Merge" button activates, one review approves, squash-merge. main now contains only CI-passed commits.
Verifying protection works
# should be rejected even for admins:
git checkout main && git commit --allow-empty -m "direct push test" && git push
# expect: ! [remote rejected] ... protected branch hook declined
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Merge button never enables, checks stuck "pending" | Webhook for pull_request not reaching Woodpecker | Test webhook delivery in Forgejo repo settings; re-sync per webhooks |
| Checks pass but merge still blocked | Branch outdated vs main |
Update branch (merge/rebase main), wait for new pipeline run |
| Required check name doesn't match | Step/workflow renamed in .woodpecker.yml |
Keep names stable; update rule when renaming deliberately |
| Admin accidentally locked out entirely | No one in merge whitelist | Add at least one admin account before clearing whitelists |
| Protection bypassed via force-push to featureβmain fast-forward | Rule pattern mismatch | Confirm pattern matches exactly (main, anchored); avoid overly broad globs |
| Matrix legs block merge unpredictably | One leg flaky, all legs required | Fix or trim matrix (matrix-builds); don't drop the requirement |
Next Steps / Ops Actions
- Make sure what merges is also clean, not just tested: security-scanning
- Alert when pipelines required for merge start failing: ci-monitoring
- Review who holds admin rights periodically: rbac-basics
Sources & Related
External references consulted:
- https://docs.gitea.com/usage/protected-tags (Forgejo shares Gitea's protection model)
Related knowledge-base articles:
- forgejo-installation
- woodpecker-installation
- repository-setup
- user-management
- merge-strategies
- two-factor-authentication
Change Log
2026-08-26
- Initial creation by KB writing session.