Skip to content

Commit c39116b

Browse files
authored
Merge pull request #27 from aRustyDev/pr/just-protect-repo
feat(just): Add repository branch protection justfile entries
2 parents fc59aa1 + b95c661 commit c39116b

10 files changed

Lines changed: 719 additions & 393 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
---
2+
id: 2769C84B-479C-4EE3-9970-0AF4A973C82E
3+
title: "GitHub Repository Protection Strategy"
4+
status: "✅ Active"
5+
date: 2025-01-13
6+
author: aRustyDev
7+
type: strategy
8+
related:
9+
- ../adr/frontmatter-standard.md
10+
---
11+
12+
# GitHub Repository Protection Strategy
13+
14+
## Overview
15+
16+
This document defines the strategy for protecting GitHub repositories using branch rulesets. The goal is to enforce a consistent, secure workflow across all repositories that prevents accidental or unauthorized changes to critical branches.
17+
18+
---
19+
20+
## Branch Model
21+
22+
### Protected Branches
23+
24+
| Branch | Purpose | Protection Level |
25+
| ------------- | ---------------------------------------- | ---------------- |
26+
| `main` | Production-ready code, release source | Maximum |
27+
| `integration` | Pre-release staging, feature integration | High |
28+
29+
### Workflow
30+
31+
```
32+
feature/* ──────┐
33+
bugfix/* ──────┼──► PR ──► integration ──► PR ──► main
34+
hotfix/* ──────┘
35+
```
36+
37+
1. **Feature Development**: Work on `feature/*`, `bugfix/*`, or `hotfix/*` branches
38+
2. **Integration**: Merge to `integration` via Pull Request with review
39+
3. **Release**: Merge `integration` to `main` via Pull Request with review
40+
41+
---
42+
43+
## Protection Rules
44+
45+
### Main Branch Protection
46+
47+
The `main` branch receives the highest level of protection:
48+
49+
| Rule | Setting | Rationale |
50+
| --------------------- | ------------- | ------------------------------------------- |
51+
| Direct pushes | ❌ Blocked | All changes must go through PR |
52+
| Force pushes | ❌ Blocked | Preserve history integrity |
53+
| Branch deletion | ❌ Blocked | Prevent accidental loss |
54+
| Required PR reviews | ✅ 1 reviewer | Ensure code quality and knowledge sharing |
55+
| Dismiss stale reviews | ✅ Enabled | Reviews must be current with latest changes |
56+
| Resolve conversations | ✅ Required | All feedback must be addressed |
57+
| Linear history | ✅ Required | Clean, traceable commit history |
58+
59+
### Integration Branch Protection
60+
61+
The `integration` branch serves as a staging area before production:
62+
63+
| Rule | Setting | Rationale |
64+
| --------------------- | ------------- | ------------------------------------------- |
65+
| Direct pushes | ❌ Blocked | All changes must go through PR |
66+
| Force pushes | ❌ Blocked | Preserve history integrity |
67+
| Branch deletion | ❌ Blocked | Prevent accidental loss |
68+
| Required PR reviews | ✅ 1 reviewer | Ensure code quality |
69+
| Dismiss stale reviews | ✅ Enabled | Reviews must be current with latest changes |
70+
| Resolve conversations | ✅ Required | All feedback must be addressed |
71+
72+
---
73+
74+
## Implementation
75+
76+
### Ruleset Files
77+
78+
Rulesets are stored as JSON files in `.github/rulesets/`. The rulesets are split into two categories to enable granular bypass permissions:
79+
80+
1. **Core Protection Rulesets** (no bypass): Enforce fundamental branch safety rules
81+
2. **PR Review Rulesets** (with bypass): Enforce review requirements, allowing designated users to bypass when needed
82+
83+
```
84+
.github/rulesets/
85+
├── main-branch-protection.json # Core rules: deletion, force-push, linear history
86+
├── main-pr-reviews.json # PR review requirements (bypassable)
87+
├── integration-branch-protection.json # Core rules: deletion, force-push
88+
└── integration-pr-reviews.json # PR review requirements (bypassable)
89+
```
90+
91+
#### Why Split Rulesets?
92+
93+
GitHub Rulesets bypass actors work at the **ruleset level**, not at the individual rule level. To allow a user to bypass only the `required_approving_review_count` while still enforcing other rules (like preventing force pushes), the rules must be separated into different rulesets.
94+
95+
### Justfile Recipes
96+
97+
The following recipes are available for managing repository protection:
98+
99+
| Recipe | Description |
100+
| ---------------------------------- | -------------------------------------- |
101+
| `just protect-repo <repo>` | Apply all branch protection rulesets |
102+
| `just apply-ruleset <repo> <file>` | Apply a single ruleset from JSON file |
103+
| `just unprotect-repo <repo>` | Remove all rulesets (use with caution) |
104+
| `just list-rulesets <repo>` | List all rulesets for a repository |
105+
106+
### Usage Examples
107+
108+
```bash
109+
# Protect a repository (applies all rulesets)
110+
just protect-repo aRustyDev/my-repo
111+
112+
# Apply a single ruleset
113+
just apply-ruleset aRustyDev/my-repo .github/rulesets/main-branch-protection.json
114+
115+
# View current protection status
116+
just list-rulesets aRustyDev/my-repo
117+
118+
# Remove all protections
119+
just unprotect-repo aRustyDev/my-repo
120+
```
121+
122+
---
123+
124+
## GitHub Rulesets API
125+
126+
### Why Rulesets Over Branch Protection Rules?
127+
128+
GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch protection rules:
129+
130+
| Feature | Legacy Protection | Rulesets |
131+
| ------------------------ | ----------------- | ---------------- |
132+
| Target multiple branches | ❌ One at a time | ✅ Pattern-based |
133+
| Organization-wide rules | ❌ Repo-only | ✅ Org-level |
134+
| Import/Export as JSON | ❌ API only | ✅ Native JSON |
135+
| Bypass permissions | Limited | ✅ Fine-grained |
136+
| Audit logging | Basic | ✅ Enhanced |
137+
138+
### API Endpoints
139+
140+
| Operation | Method | Endpoint |
141+
| -------------- | ------ | ------------------------------------- |
142+
| List rulesets | GET | `/repos/{owner}/{repo}/rulesets` |
143+
| Create ruleset | POST | `/repos/{owner}/{repo}/rulesets` |
144+
| Get ruleset | GET | `/repos/{owner}/{repo}/rulesets/{id}` |
145+
| Update ruleset | PUT | `/repos/{owner}/{repo}/rulesets/{id}` |
146+
| Delete ruleset | DELETE | `/repos/{owner}/{repo}/rulesets/{id}` |
147+
148+
### Ruleset JSON Schema
149+
150+
**Core Protection Ruleset** (no bypass):
151+
152+
```json
153+
{
154+
"name": "main-branch-protection",
155+
"target": "branch",
156+
"enforcement": "active",
157+
"conditions": {
158+
"ref_name": {
159+
"include": ["refs/heads/main"],
160+
"exclude": []
161+
}
162+
},
163+
"rules": [
164+
{ "type": "deletion" },
165+
{ "type": "non_fast_forward" },
166+
{ "type": "required_linear_history" }
167+
],
168+
"bypass_actors": []
169+
}
170+
```
171+
172+
**PR Review Ruleset** (with bypass):
173+
174+
```json
175+
{
176+
"name": "main-pr-reviews",
177+
"target": "branch",
178+
"enforcement": "active",
179+
"conditions": {
180+
"ref_name": {
181+
"include": ["refs/heads/main"],
182+
"exclude": []
183+
}
184+
},
185+
"rules": [
186+
{
187+
"type": "pull_request",
188+
"parameters": {
189+
"required_approving_review_count": 1,
190+
"dismiss_stale_reviews_on_push": true,
191+
"require_code_owner_review": false,
192+
"require_last_push_approval": true,
193+
"required_review_thread_resolution": true,
194+
"allowed_merge_methods": ["merge", "squash", "rebase"]
195+
}
196+
}
197+
],
198+
"bypass_actors": [
199+
{
200+
"actor_id": 5,
201+
"actor_type": "RepositoryRole",
202+
"bypass_mode": "pull_request"
203+
}
204+
]
205+
}
206+
```
207+
208+
### Bypass Actor Configuration
209+
210+
| Property | Value | Description |
211+
| ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- |
212+
| `actor_id` | Role/Team/Integration ID | Numeric ID (see below for role IDs) |
213+
| `actor_type` | `Integration`, `OrganizationAdmin`, `RepositoryRole`, `Team`, `DeployKey` | Type of actor (Note: `User` is NOT valid for repository rulesets) |
214+
| `bypass_mode` | `always`, `pull_request`, `exempt` | When bypass applies |
215+
216+
**Repository Role IDs:**
217+
218+
| ID | Role | Description |
219+
| --- | ---------------- | --------------------------------------------------- |
220+
| 1 | Maintain | Can manage repo without access to sensitive actions |
221+
| 2 | Write | Can push to non-protected branches |
222+
| 3 | Admin | Full access to the repository |
223+
| 5 | Repository Admin | Owner role for personal repositories |
224+
225+
**Bypass Modes:**
226+
227+
- `always`: Bypass all rules at all times
228+
- `pull_request`: Only bypass rules on pull requests (recommended for review bypasses)
229+
- `exempt`: Rules not run, no audit entry created
230+
231+
---
232+
233+
## Available Rule Types
234+
235+
| Rule Type | Description |
236+
| ------------------------- | ------------------------------------------------ |
237+
| `deletion` | Prevent branch deletion |
238+
| `non_fast_forward` | Prevent force pushes (history rewrite) |
239+
| `pull_request` | Require pull requests with configurable reviews |
240+
| `required_linear_history` | Require linear commit history (no merge commits) |
241+
| `required_signatures` | Require signed commits |
242+
| `required_status_checks` | Require CI/CD checks to pass |
243+
| `update` | Prevent non-admin updates |
244+
245+
---
246+
247+
## Prerequisites
248+
249+
1. **GitHub CLI**: Authenticated with admin access to the target repository
250+
251+
```bash
252+
gh auth status
253+
```
254+
255+
2. **jq**: For JSON processing
256+
257+
```bash
258+
jq --version
259+
```
260+
261+
3. **Repository Admin Access**: Required to create/modify rulesets
262+
263+
---
264+
265+
## Troubleshooting
266+
267+
### Common Issues
268+
269+
| Issue | Solution |
270+
| ------------------------- | ----------------------------------------------- |
271+
| "Resource not accessible" | Ensure you have admin access to the repository |
272+
| "Ruleset already exists" | The recipe will update existing rulesets |
273+
| "Branch doesn't exist" | `protect-integration` creates branch if missing |
274+
| "Invalid ruleset format" | Validate JSON in `.github/rulesets/` files |
275+
276+
### Verification
277+
278+
After applying protection, verify with:
279+
280+
```bash
281+
# List applied rulesets
282+
just list-rulesets owner/repo
283+
284+
# Test protection (should fail)
285+
git push origin main # Should be rejected
286+
287+
# Verify in GitHub UI
288+
# Settings → Rules → Rulesets
289+
```
290+
291+
---
292+
293+
## Security Considerations
294+
295+
1. **Bypass Actors**: Bypass actors are configured only on PR review rulesets, allowing designated users to merge without approval while still being subject to core branch protections (no force push, no deletion). The bypass uses `"bypass_mode": "pull_request"` to limit bypass capability to PR merges only.
296+
297+
2. **Enforcement Level**: Use `"enforcement": "active"` for production. Use `"enforcement": "evaluate"` for testing without blocking.
298+
299+
3. **Review Requirements**: Minimum 1 reviewer is recommended. Increase for sensitive repositories.
300+
301+
4. **Emergency Access**: Document procedures for legitimate emergency direct pushes (requires temporary ruleset modification).
302+
303+
---
304+
305+
## References
306+
307+
- [GitHub Rulesets Documentation](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets)
308+
- [GitHub REST API - Rulesets](https://docs.github.com/en/rest/repos/rules)
309+
- [Migrating from Branch Protection to Rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets#about-rulesets-branch-protection-rules-and-protected-tags)

.github/.todo.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,17 @@
55
- (maybe?) mcp-contribute-feature
66
- (maybe?) mcp-contribute-fix
77
- (maybe?) mcp-bug-report
8+
9+
## Rulesets
10+
11+
- enforce merge to integration branch only
12+
- enforce allowed branch creation patterns 'pr/\*'
13+
- Bypasses:
14+
- dependabot: enforce allowed branch creation patterns 'dependency/\*'
15+
- auto-merge bots: enforce allowed branch creation patterns 'dependency/\*'
16+
- [Prevent Unauthorized CI/CD Changes](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#1-prevent-unauthorized-cicd-changes)
17+
- [Block Binary Files from Being Pushed into the Repository](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#3-block-binary-files-from-being-pushed-into-the-repository)
18+
- [Restrict Commits to Authors with Company Email Addresses](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#4-restrict-commits-to-authors-with-company-email-addresses)
19+
- [Enforce Branch Naming Patterns](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#5-enforce-branch-naming-patterns)
20+
- [Protecting Production Branch](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#6-protecting-the-production-branch)
21+
- [Lint Enforcement](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#7-ensure-code-adheres-to-linting-standards-before-merging)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "integration-branch-protection",
3+
"target": "branch",
4+
"enforcement": "active",
5+
"conditions": {
6+
"ref_name": {
7+
"include": ["refs/heads/integration"],
8+
"exclude": []
9+
}
10+
},
11+
"rules": [
12+
{
13+
"type": "deletion"
14+
},
15+
{
16+
"type": "non_fast_forward"
17+
}
18+
],
19+
"bypass_actors": []
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "integration-pr-reviews",
3+
"target": "branch",
4+
"enforcement": "active",
5+
"conditions": {
6+
"ref_name": {
7+
"include": ["refs/heads/integration"],
8+
"exclude": []
9+
}
10+
},
11+
"rules": [
12+
{
13+
"type": "pull_request",
14+
"parameters": {
15+
"required_approving_review_count": 1,
16+
"dismiss_stale_reviews_on_push": true,
17+
"require_code_owner_review": false,
18+
"require_last_push_approval": false,
19+
"required_review_thread_resolution": true,
20+
"allowed_merge_methods": ["merge", "squash", "rebase"]
21+
}
22+
}
23+
],
24+
"bypass_actors": [
25+
{
26+
"actor_id": 5,
27+
"actor_type": "RepositoryRole",
28+
"bypass_mode": "pull_request"
29+
}
30+
]
31+
}

0 commit comments

Comments
 (0)