Required Checks, Approvals und Force-Push-Schutz für main
Ein main-Branch ohne Schutz ist nur einen falschen Force-Push von einem Totalschaden entfernt. Dieser Artikel zeigt, was Protected Branches auf GitHub und GitLab wirklich verhindern, wie Required Status Checks und Approval-Regeln funktionieren, welche Rolle CODEOWNERS spielt und mit welcher Baseline Magento- und Hyvä-Agenturen ihren main-Branch praxistauglich absichern, ohne das Team auszubremsen.
Inhaltsverzeichnis
- 1. Protected Branches: mehr als Bürokratie
- 2. Was Protected Branches konkret verhindern
- 3. Required Status Checks: CI muss grün sein
- 4. Required Reviews: Approvals und CODEOWNERS
- 5. Force-Push und Branch-Löschung technisch blockieren
- 6. Branch Protection in GitHub einrichten
- 7. Protected Branches in GitLab konfigurieren
- 8. Strenge vs. Team-Friktion: das richtige Maß finden
- 9. Unprotected vs. Protected Branch im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Protected Branches: mehr als Bürokratie
Ein main-Branch ohne Schutz sieht in der Theorie nach einem trivialen Detail aus, ist in der Praxis aber die Stelle, an der ein einziger unbedachter Befehl den gesamten Produktionsstand eines Magento-Shops zerstören kann. Protected Branches sind keine Bürokratie, sondern eine serverseitige Absicherung: Git selbst kennt kein Konzept von wichtig oder unwichtig, jeder Branch ist technisch gleichwertig, bis eine Regel explizit etwas anderes festlegt. Genau diese Regel setzen GitHub und GitLab mit Branch Protection um.
Dieser Artikel deckt ab, was Protected Branches tatsächlich verhindern, wie Required Status Checks und Required Reviews zusammenspielen, welche Rolle eine CODEOWNERS-Datei spielt und wie man die Balance zwischen Sicherheit und Team-Geschwindigkeit findet. Am Ende steht eine konkrete Baseline-Konfiguration, wie sie sich für main oder production in einer Magento- und Hyvä-Agentur wie Mironsoft in der Praxis bewährt hat.
2. Was Protected Branches konkret verhindern
Branch Protection wirkt an drei Stellen gleichzeitig, und alle drei adressieren ein reales Schadensszenario. Erstens verhindert sie direkte Pushes auf den geschützten Branch: Änderungen müssen zwingend über einen Pull beziehungsweise Merge Request laufen, selbst wenn ein Entwickler technisch Schreibrechte auf das Repository hat. Zweitens blockiert sie Force-Pushes, die die Commit-Historie überschreiben und dabei fremde Commits unwiderruflich verschwinden lassen können, etwa nach einem missglückten Rebase. Drittens schützt sie den Branch vor versehentlicher Löschung, was insbesondere bei automatisierten Aufräum-Skripten oder CI-Jobs mit zu weitreichenden Rechten relevant ist.
Alle drei Mechanismen greifen serverseitig, nicht clientseitig: Ein lokaler Git-Client kennt die Regel nicht und lässt den Befehl zunächst zu, die Ablehnung kommt erst als Antwort des Servers beim Push. Das ist ein wichtiger Unterschied zu clientseitigen Git-Hooks, die ein Entwickler lokal umgehen kann. Branch Protection lässt sich von einem einzelnen Entwickler nicht aushebeln, sofern sie korrekt konfiguriert ist.
3. Required Status Checks: CI muss grün sein
Required Status Checks koppeln den Merge-Button direkt an das Ergebnis der CI-Pipeline. Solange ein definierter Check nicht erfolgreich durchgelaufen ist, bleibt der Merge-Button in GitHub deaktiviert, beziehungsweise blockiert GitLab den Merge über die Pipeline-must-succeed-Einstellung. Das verhindert das häufigste Antipattern in ungeschützten Repositories: einen roten Build mit der Begründung "wird schon gehen" trotzdem zu mergen, weil Deadline-Druck wichtiger erschien als ein fehlgeschlagener Test.
Die Tücke liegt im Detail: GitHub identifiziert einen Required Check über seinen exakten Namen, also den Job-Namen aus der GitHub-Actions-Workflow-Datei, nicht über den Namen der Workflow-Datei selbst. Wird ein Job umbenannt oder eine Pipeline umstrukturiert, verschwindet der erwartete Check und blockiert den Merge dauerhaft, weil GitHub auf ein Ergebnis wartet, das nie mehr gemeldet wird. Bei GitLab greift stattdessen die projektweite Einstellung "Pipelines must succeed", kombiniert mit optionalen Merge-Request-Approval-Regeln für einzelne Jobs.
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
build-and-test:
# This job name is what the branch protection rule references,
# not the workflow file name "ci.yml".
name: build-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Run PHPStan
run: vendor/bin/phpstan analyse app/code --level=5
- name: Run PHPUnit
run: vendor/bin/phpunit --testsuite unit
4. Required Reviews: Approvals und CODEOWNERS
Die Anzahl erforderlicher Approvals ist die zweite tragende Säule von Branch Protection. Ein bis zwei Reviewer vor dem Merge sind der in der Praxis gängige Wert, mehr erzeugt in kleinen Teams meist nur Wartezeit ohne zusätzlichen Sicherheitsgewinn. Mit einer CODEOWNERS-Datei im Repository-Root oder unter .github/ beziehungsweise .gitlab/ lässt sich zusätzlich festlegen, welches Teammitglied oder Team für welchen Pfad automatisch als Pflicht-Reviewer vorgeschlagen wird, etwa das Frontend-Team für Änderungen im Hyvä-Theme und das Backend-Team für Änderungen an der db_schema.xml.
Ein oft übersehener Schalter ist "Dismiss stale approvals on new commits". Ohne ihn bleibt ein Approval gültig, selbst wenn nach der Freigabe noch zusätzliche Commits gepusht werden, die der Reviewer nie gesehen hat. Das öffnet eine Lücke, über die nachträglich ungeprüfter Code in einen bereits freigegebenen Pull Request eingeschleust werden kann. Für main oder production sollte dieser Schalter praktisch immer aktiv sein, auch wenn er kleine Reibung erzeugt, wenn Reviewer nach jedem Push erneut freigeben müssen.
# .github/CODEOWNERS - automatic reviewer assignment for pull requests
# Order matters: the last matching pattern takes precedence
# Default owners for everything in the repo
* @mironsoft/core-team
# Magento module code requires backend review
/app/code/ @mironsoft/backend-team
/app/code/Mironsoft/SeoSuite/ @mironsoft/seo-team
# Hyva theme and frontend assets require frontend review
/app/design/frontend/ @mironsoft/frontend-team
# CI/CD and deployment configuration requires DevOps sign-off
/.github/workflows/ @mironsoft/devops-team
/compose.yaml @mironsoft/devops-team
# Database schema changes always need a second pair of eyes
/app/code/**/etc/db_schema.xml @mironsoft/backend-team @mironsoft/devops-team
5. Force-Push und Branch-Löschung technisch blockieren
Auf GitHub steuert die Option "Allow force pushes" separat von den übrigen Regeln, ob überhaupt ein Force-Push auf den geschützten Branch möglich ist, standardmäßig ist sie deaktiviert. Auf GitLab existiert dieselbe Einstellung als "Allow force push" bei der Protected-Branch-Konfiguration. Versucht ein Entwickler trotzdem einen Force-Push, lehnt der Server den Push mit einer eindeutigen Fehlermeldung ab, der lokale Git-Client zeigt "remote rejected" und der Push schlägt vollständig fehl, ohne dass Commits verloren gehen.
Branch-Löschung wird über eine separate Einstellung geschützt: "Allow deletions" auf GitHub, beziehungsweise die Protected-Branch-Regel auf GitLab, die verhindert, dass ein Branch über die Kommandozeile oder das Web-Interface gelöscht wird, solange er als geschützt markiert ist. Diese Absicherung ist besonders wichtig für Release-Branches, die nach einem Merge fälschlicherweise als "erledigt" markiert und automatisiert aufgeräumt werden könnten.
$ git push --force origin main
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 8 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 1.02 KiB | 1.02 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0)
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Cannot force-push to a protected branch
To github.com:mironsoft/shop.git
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:mironsoft/shop.git'
# GitLab shows a comparable rejection:
$ git push --force origin main
remote: GitLab: You are not allowed to force push code to a protected branch on this project.
To gitlab.com:mironsoft/shop.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'gitlab.com:mironsoft/shop.git'
6. Branch Protection in GitHub einrichten
Die schnellste und reproduzierbarste Methode, Branch Protection auf GitHub einzurichten, ist die GitHub-CLI oder die REST-API, statt die Einstellung manuell im Web-Interface zu klicken. Das hat den Vorteil, dass sich die Konfiguration versionieren und für mehrere Repositories identisch ausrollen lässt, etwa über ein internes Setup-Skript für neue Magento-Projekte.
Das folgende Beispiel setzt eine vollständige Baseline: zwei Pflicht-Approvals inklusive CODEOWNERS-Pflicht, verworfene Approvals bei neuen Commits, einen Required Status Check namens build-and-test, deaktivierte Force-Pushes und Löschungen sowie optional Required Linear History.
# Configure branch protection for main via the GitHub CLI
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
repos/mironsoft/shop/branches/main/protection \
--input - <<'EOF'
{
"required_status_checks": {
"strict": true,
"contexts": ["build-and-test"]
},
"enforce_admins": false,
"required_pull_request_reviews": {
"required_approving_review_count": 2,
"require_code_owner_reviews": true,
"dismiss_stale_reviews": true
},
"restrictions": null,
"required_linear_history": true,
"allow_force_pushes": false,
"allow_deletions": false
}
EOF
7. Protected Branches in GitLab konfigurieren
GitLab modelliert Branch Protection etwas anders als GitHub: Statt eines einzelnen "protected" Schalters mit mehreren Unteroptionen definiert die Protected-Branches-API separate Access Level für Push und Merge. Ein push_access_level von 0 bedeutet "No one", also dass niemand direkt pushen darf, während ein merge_access_level von 30 Merges über einen Merge Request ab der Developer-Rolle erlaubt.
Die Pflicht zu grünen Pipelines wird bei GitLab nicht über die Protected-Branches-API gesetzt, sondern über die projektweite Merge-Request-Einstellung "Pipelines must succeed" in den General Settings unter Merge Requests. Code-Owner-Approval lässt sich sowohl projektweit als auch pro geschütztem Branch über code_owner_approval_required aktivieren, was CODEOWNERS-Dateien auf GitLab funktional gleichwertig zu GitHub macht.
# Configure a protected branch for main via the GitLab API
curl --request POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/protected_branches" \
--data "name=main" \
--data "push_access_level=0" \
--data "merge_access_level=30" \
--data "unprotect_access_level=40" \
--data "allow_force_push=false" \
--data "code_owner_approval_required=true"
# push_access_level=0 means "No one": merges only via an approved MR
# merge_access_level=30 restricts merges to the Developer role and above
8. Strenge vs. Team-Friktion: das richtige Maß finden
Zu strenge Branch Protection bremst gerade kleine Teams spürbar aus: Wer mit zwei Entwicklern arbeitet und zwei Pflicht-Approvals verlangt, blockiert sich im Zweifel selbst, wenn beide gleichzeitig im Urlaub oder krank sind. Wer zusätzlich enforce_admins aktiviert, kann im Ernstfall nicht einmal als Repository-Owner einen kritischen Hotfix durchdrücken, ohne die Regel vorübergehend zu ändern. Zu lockere Protection wiederum verfehlt den eigentlichen Zweck komplett: Ein Required Check, der nicht wirklich required ist, weil Admins ihn umgehen können, schützt am Ende niemanden.
Als Faustregel hat sich in der Praxis eine Staffelung nach Teamgröße bewährt. Solo-Entwickler oder Zwei-Personen-Teams fahren gut mit einem Pflicht-Approval, aktivierten Required Checks und deaktiviertem enforce_admins, damit im Notfall noch reagiert werden kann. Agentur-Teams mit fünf bis zehn Entwicklern profitieren von zwei Pflicht-Approvals, CODEOWNERS-Pflicht für kritische Pfade wie db_schema.xml und Deployment-Konfiguration, sowie einem klar dokumentierten Ausnahmeprozess für echte Hotfixes, statt die Protection für alle Fälle pauschal aufzuweichen.
9. Unprotected vs. Protected Branch im Vergleich
Die folgende Übersicht stellt einen ungeschützten main-Branch der empfohlenen Baseline für Magento- und Hyvä-Projekte gegenüber. Der Unterschied liegt selten in exotischen Einstellungen, sondern in der konsequenten Kombination weniger Grundregeln.
| Dimension | Unprotected main | Protected main (Baseline) |
|---|---|---|
| Direkte Pushes | Jeder mit Schreibrechten kann direkt pushen | Nur über Pull Request, kein Direct Push |
| Force-Push | Erlaubt, überschreibt Historie und Kollegenarbeit | Blockiert, allow_force_pushes: false |
| Branch-Löschung | Jeder kann den Branch löschen | Geschützt, allow_deletions: false |
| Required Approvals | Keine, Merge ohne Review möglich | 1-2 Approvals inkl. CODEOWNERS-Pflicht |
| Required CI Checks | Merge auch bei rotem Build möglich | Merge blockiert, bis build-and-test grün ist |
Für main oder production in einer Agentur-Umgebung hat sich folgende Baseline bewährt: Pull Request Pflicht ohne Ausnahme, ein bis zwei Approvals abhängig von der Teamgröße, Required Status Checks für Build und Tests, deaktivierte Force-Pushes, eingeschränkte direkte Push-Rechte über restrictions beziehungsweise push_access_level, und optional Required Linear History für ein sauberes, bisektionsfreundliches Log. Diese Kombination deckt die relevanten Schadensszenarien ab, ohne das Team durch übertriebene Bürokratie auszubremsen.
Mironsoft
Git-Workflows, Branch Protection und CI/CD-Pipelines für Magento-Teams
Branch Protection sauber eingerichtet, ohne das Team auszubremsen?
Wir richten Required Status Checks, Approval-Regeln und CODEOWNERS für main und production ein, passend zur Größe eures Teams und zu eurem Magento- beziehungsweise Hyvä-Deployment-Workflow.
Branch-Protection-Audit
Bestehende main-Konfiguration auf GitHub oder GitLab gegen Best Practices prüfen
CI/CD-Integration
Required Status Checks passend zu eurer Pipeline einrichten und dokumentieren
CODEOWNERS-Setup
Reviewer-Zuordnung nach Modulen und Teams für Magento- und Hyvä-Code
10. Zusammenfassung
Protected Branches lösen ein konkretes Problem: main oder production darf nicht von einem einzelnen unbedachten Befehl abhängen. Direct Pushes, Force-Pushes und Branch-Löschungen werden serverseitig blockiert, Required Status Checks stellen sicher, dass nur getesteter Code gemerged wird, und Required Reviews mit CODEOWNERS sorgen dafür, dass die richtigen Personen jede Änderung sehen, bevor sie produktiv wird.
Der entscheidende Punkt ist das richtige Maß: Zu strenge Regeln bremsen kleine Teams aus, zu lockere Regeln verfehlen den Zweck. Eine Baseline aus ein bis zwei Approvals, Pflicht-CI-Checks, deaktivierten Force-Pushes und einem dokumentierten Ausnahmeprozess für Hotfixes deckt die meisten Magento- und Hyvä-Projekte praxistauglich ab, ohne Entwickler unnötig zu bremsen.
Protected Branches richtig konfigurieren - Das Wichtigste auf einen Blick
Was Protection verhindert
Direct Pushes, Force-Pushes und Branch-Löschung auf main werden serverseitig blockiert, unabhängig vom lokalen Git-Client.
Required Checks
CI muss grün sein, bevor gemerged werden kann. Der Check-Name muss exakt zum Job aus der Workflow-Datei passen.
Approvals & CODEOWNERS
1-2 Pflicht-Reviewer, automatische Zuordnung über CODEOWNERS, Approvals bei neuen Commits verwerfen.
Baseline für Agenturen
PR-Pflicht, 1-2 Approvals, CI-Pflicht, kein Force-Push, kein Direct Push, dokumentierter Hotfix-Prozess.