Skip to content
This repository was archived by the owner on Apr 5, 2026. It is now read-only.

Commit fb2d82f

Browse files
HerbHallclaude
andauthored
feat: standardize Copilot auto-review + contributor gating (#194) (#207)
- Update copilot-ruleset.json to include PR review rule (1 required review), admin bypass, and squash-only merge policy - Add scripts/copilot-review-setup.sh with audit/setup/audit-all modes - Document three-layer protection model in copilot-integration.md (access control + CI checks + Copilot review) The audit script checks 7 compliance criteria per repo: 1. Auto-merge enabled 2. Copilot PR Review ruleset exists and is active 3. copilot_code_review rule with review_on_push: true 4. pull_request rule with required_approving_review_count: 1 5. Admin bypass actor configured 6. Branch protection has no review requirement (avoids double gate) 7. CODEOWNERS file exists Closes #194 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4ebed4c commit fb2d82f

3 files changed

Lines changed: 303 additions & 2 deletions

File tree

docs/copilot-integration.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,81 @@ DevKit provides ready-to-copy templates in `project-templates/`:
133133
| copilot-setup-steps (Fullstack) | `project-templates/copilot-setup-steps-fullstack.yml` | Go + React combined |
134134
| CodeQL | `project-templates/codeql.yml` | Security scanning workflow |
135135

136+
## Three-Layer Protection Model
137+
138+
All projects use three complementary layers for PR quality:
139+
140+
### Layer 1: Access Control (repo permissions)
141+
142+
Contributors cannot merge -- only the owner has write access. This is the gatekeeper.
143+
No workflow or ruleset needed; repo permissions handle it.
144+
145+
### Layer 2: CI Status Checks (branch protection)
146+
147+
Branch protection requires CI jobs to pass before merge. Configured via the GitHub API:
148+
149+
```bash
150+
gh api repos/OWNER/REPO/branches/main/protection -X PUT --input - << 'JSON'
151+
{
152+
"required_status_checks": { "strict": true, "contexts": ["Build", "Lint", "Test"] },
153+
"enforce_admins": false,
154+
"required_pull_request_reviews": null,
155+
"restrictions": null
156+
}
157+
JSON
158+
```
159+
160+
Branch protection handles CI checks **only** -- no review requirement here (that's Layer 3).
161+
Adding a review requirement to branch protection AND the ruleset creates a double gate.
162+
163+
### Layer 3: Copilot Auto-Review (ruleset)
164+
165+
A GitHub ruleset named "Copilot PR Review" handles code review:
166+
167+
- **Owner PRs**: Copilot reviews automatically; auto-merge when CI + Copilot approve (hands-free)
168+
- **Contributor PRs**: Copilot reviews automatically; owner reviews after Copilot approves; owner merges manually
169+
- **Admin bypass**: Solo maintainers can merge even if Copilot hasn't reviewed yet
170+
171+
Template: `project-templates/copilot-ruleset.json`
172+
173+
Key settings:
174+
175+
- `required_approving_review_count: 1` -- Copilot satisfies this for owner PRs
176+
- `dismiss_stale_reviews_on_push: true` -- forces re-review on new pushes
177+
- `copilot_code_review` with `review_on_push: true` -- triggers review on each push
178+
- Admin role (RepositoryRole id 5) as bypass actor
179+
- `allowed_merge_methods: ["squash"]` -- squash-only merge
180+
181+
### Setup and Audit
182+
183+
```bash
184+
# Set up Copilot auto-review on a repo
185+
bash scripts/copilot-review-setup.sh setup OWNER/REPO
186+
187+
# Audit a single repo
188+
bash scripts/copilot-review-setup.sh audit OWNER/REPO
189+
190+
# Audit all repos for an owner
191+
bash scripts/copilot-review-setup.sh audit-all OWNER
192+
```
193+
194+
### Manual Steps After Setup
195+
196+
The Copilot auto-review UI toggle cannot be set via API:
197+
198+
1. Go to repo Settings > Rules > Rulesets > "Copilot PR Review"
199+
2. Click Edit on "Require a pull request before merging"
200+
3. Under "Additional settings", enable "Require review from GitHub Copilot"
201+
4. Also enable "Review new pushes" for re-review on each push
202+
5. Create a test PR with a real file change to verify Copilot reviews it
203+
204+
### Why These Specific Settings
205+
206+
- **`required_approving_review_count: 1`** (not 0 or 2): Copilot's approval counts as the 1 required review for owner PRs. Setting to 0 would skip review entirely. Setting to 2 would block solo maintainers.
207+
- **Admin bypass**: Required for solo maintainers who need to merge when Copilot is unavailable or reviewing incorrectly.
208+
- **No review in branch protection**: Branch protection review + ruleset review = double gate. Use rulesets only for reviews, branch protection only for CI checks.
209+
- **`review_on_push: true`**: Only fires on push events, not open/reopen. Forces re-review when code changes after initial review.
210+
136211
## Per-Project Rollout Checklist
137212

138213
For each existing project, complete in order:

project-templates/copilot-ruleset.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "Copilot Code Review",
2+
"name": "Copilot PR Review",
33
"enforcement": "active",
44
"target": "branch",
55
"conditions": {
@@ -8,8 +8,26 @@
88
"exclude": []
99
}
1010
},
11-
"bypass_actors": [],
11+
"bypass_actors": [
12+
{
13+
"actor_id": 5,
14+
"actor_type": "RepositoryRole",
15+
"bypass_mode": "always"
16+
}
17+
],
1218
"rules": [
19+
{
20+
"type": "pull_request",
21+
"parameters": {
22+
"required_approving_review_count": 1,
23+
"dismiss_stale_reviews_on_push": true,
24+
"required_reviewers": [],
25+
"require_code_owner_review": false,
26+
"require_last_push_approval": false,
27+
"required_review_thread_resolution": false,
28+
"allowed_merge_methods": ["squash"]
29+
}
30+
},
1331
{
1432
"type": "copilot_code_review",
1533
"parameters": {

scripts/copilot-review-setup.sh

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# copilot-review-setup.sh -- Set up or audit Copilot auto-review on a GitHub repo.
5+
#
6+
# Usage:
7+
# ./scripts/copilot-review-setup.sh audit OWNER/REPO # check compliance
8+
# ./scripts/copilot-review-setup.sh setup OWNER/REPO # create ruleset + enable auto-merge
9+
# ./scripts/copilot-review-setup.sh audit-all OWNER # audit all non-archived repos
10+
#
11+
# Requires: gh (GitHub CLI) authenticated with admin access.
12+
13+
ACTION="${1:-}"
14+
TARGET="${2:-}"
15+
16+
if [[ -z "$ACTION" || -z "$TARGET" ]]; then
17+
echo "Usage: $0 {audit|setup|audit-all} OWNER/REPO_OR_OWNER"
18+
exit 1
19+
fi
20+
21+
# Colors
22+
RED='\033[0;31m'
23+
GREEN='\033[0;32m'
24+
YELLOW='\033[0;33m'
25+
NC='\033[0m'
26+
27+
pass() { echo -e " ${GREEN}PASS${NC}: $1"; }
28+
fail() { echo -e " ${RED}FAIL${NC}: $1"; }
29+
warn() { echo -e " ${YELLOW}WARN${NC}: $1"; }
30+
31+
# -------------------------------------------------------------------
32+
# audit_repo -- check compliance for a single repo
33+
# -------------------------------------------------------------------
34+
audit_repo() {
35+
local repo="$1"
36+
local errors=0
37+
38+
echo "=== Auditing $repo ==="
39+
40+
# 1. Auto-merge enabled
41+
local auto_merge
42+
auto_merge=$(gh api "repos/$repo" --jq '.allow_auto_merge' 2>/dev/null || echo "false")
43+
if [[ "$auto_merge" == "true" ]]; then
44+
pass "Auto-merge enabled"
45+
else
46+
fail "Auto-merge not enabled"
47+
errors=$((errors + 1))
48+
fi
49+
50+
# 2. Copilot PR Review ruleset exists and is active
51+
local ruleset_id=""
52+
local ruleset_enforcement=""
53+
while IFS=$'\t' read -r rid rname renf; do
54+
if [[ "$rname" == "Copilot PR Review" || "$rname" == "Copilot Code Review" ]]; then
55+
ruleset_id="$rid"
56+
ruleset_enforcement="$renf"
57+
break
58+
fi
59+
done < <(gh api "repos/$repo/rulesets" --jq '.[] | [.id, .name, .enforcement] | @tsv' 2>/dev/null || true)
60+
61+
if [[ -z "$ruleset_id" ]]; then
62+
fail "No Copilot review ruleset found"
63+
errors=$((errors + 1))
64+
elif [[ "$ruleset_enforcement" != "active" ]]; then
65+
fail "Copilot review ruleset exists but enforcement is '$ruleset_enforcement' (expected 'active')"
66+
errors=$((errors + 1))
67+
else
68+
pass "Copilot review ruleset exists and is active (id: $ruleset_id)"
69+
70+
# 3. Check copilot_code_review rule with review_on_push
71+
local has_copilot_review
72+
has_copilot_review=$(gh api "repos/$repo/rulesets/$ruleset_id" \
73+
--jq '.rules[] | select(.type == "copilot_code_review") | .parameters.review_on_push' 2>/dev/null || echo "")
74+
if [[ "$has_copilot_review" == "true" ]]; then
75+
pass "copilot_code_review rule with review_on_push: true"
76+
else
77+
fail "Missing or misconfigured copilot_code_review rule"
78+
errors=$((errors + 1))
79+
fi
80+
81+
# 4. Check pull_request rule with required_approving_review_count: 1
82+
local review_count
83+
review_count=$(gh api "repos/$repo/rulesets/$ruleset_id" \
84+
--jq '.rules[] | select(.type == "pull_request") | .parameters.required_approving_review_count' 2>/dev/null || echo "")
85+
if [[ "$review_count" == "1" ]]; then
86+
pass "pull_request rule with required_approving_review_count: 1"
87+
else
88+
fail "pull_request rule missing or review count is '$review_count' (expected 1)"
89+
errors=$((errors + 1))
90+
fi
91+
92+
# 5. Check admin bypass actor
93+
local has_admin_bypass
94+
has_admin_bypass=$(gh api "repos/$repo/rulesets/$ruleset_id" \
95+
--jq '.bypass_actors[] | select(.actor_id == 5 and .actor_type == "RepositoryRole") | .bypass_mode' 2>/dev/null || echo "")
96+
if [[ "$has_admin_bypass" == "always" ]]; then
97+
pass "Admin bypass actor configured"
98+
else
99+
fail "Admin bypass actor missing"
100+
errors=$((errors + 1))
101+
fi
102+
fi
103+
104+
# 6. Branch protection does NOT have required_pull_request_reviews
105+
local bp_reviews
106+
bp_reviews=$(gh api "repos/$repo/branches/main/protection/required_pull_request_reviews" \
107+
--jq '.required_approving_review_count' 2>/dev/null || echo "none")
108+
if [[ "$bp_reviews" == "none" ]]; then
109+
pass "Branch protection has no review requirement (avoids double review gate)"
110+
else
111+
warn "Branch protection has review requirement ($bp_reviews reviews) -- may conflict with ruleset"
112+
errors=$((errors + 1))
113+
fi
114+
115+
# 7. CODEOWNERS file exists
116+
local has_codeowners="false"
117+
for path in CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS; do
118+
if gh api "repos/$repo/contents/$path" --jq '.name' >/dev/null 2>&1; then
119+
has_codeowners="true"
120+
break
121+
fi
122+
done
123+
if [[ "$has_codeowners" == "true" ]]; then
124+
pass "CODEOWNERS file exists"
125+
else
126+
fail "CODEOWNERS file not found"
127+
errors=$((errors + 1))
128+
fi
129+
130+
echo ""
131+
if [[ "$errors" -eq 0 ]]; then
132+
echo -e " ${GREEN}All checks passed${NC}"
133+
else
134+
echo -e " ${RED}$errors check(s) failed${NC}"
135+
fi
136+
echo ""
137+
138+
return "$errors"
139+
}
140+
141+
# -------------------------------------------------------------------
142+
# setup_repo -- create ruleset and enable auto-merge
143+
# -------------------------------------------------------------------
144+
setup_repo() {
145+
local repo="$1"
146+
local script_dir
147+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
148+
local template="$script_dir/../project-templates/copilot-ruleset.json"
149+
150+
echo "=== Setting up Copilot auto-review on $repo ==="
151+
152+
# Enable auto-merge
153+
echo "Enabling auto-merge..."
154+
gh api "repos/$repo" -X PATCH -f allow_auto_merge=true --silent
155+
pass "Auto-merge enabled"
156+
157+
# Create ruleset from template
158+
echo "Creating Copilot PR Review ruleset..."
159+
if gh api "repos/$repo/rulesets" -X POST --input "$template" --silent 2>/dev/null; then
160+
pass "Ruleset created"
161+
else
162+
warn "Ruleset creation failed (may already exist)"
163+
fi
164+
165+
echo ""
166+
echo -e "${YELLOW}MANUAL STEPS REQUIRED:${NC}"
167+
echo "1. Go to repo Settings > Rules > Rulesets > 'Copilot PR Review'"
168+
echo "2. Click Edit on the 'Require a pull request before merging' rule"
169+
echo "3. Under 'Additional settings', enable 'Require review from GitHub Copilot'"
170+
echo " (This toggle is UI-only -- the API creates the copilot_code_review rule"
171+
echo " but the UI toggle may need to be confirmed)"
172+
echo "4. Verify: Create a test PR with a real file change, confirm Copilot reviews it"
173+
echo ""
174+
175+
# Run audit to verify
176+
echo "Running compliance audit..."
177+
audit_repo "$repo" || true
178+
}
179+
180+
# -------------------------------------------------------------------
181+
# main
182+
# -------------------------------------------------------------------
183+
case "$ACTION" in
184+
audit)
185+
audit_repo "$TARGET"
186+
;;
187+
setup)
188+
setup_repo "$TARGET"
189+
;;
190+
audit-all)
191+
OWNER="$TARGET"
192+
total=0
193+
compliant=0
194+
repos=$(gh repo list "$OWNER" --json name,isArchived --jq '.[] | select(.isArchived == false) | .name' | sort)
195+
for repo_name in $repos; do
196+
if audit_repo "$OWNER/$repo_name"; then
197+
compliant=$((compliant + 1))
198+
fi
199+
total=$((total + 1))
200+
done
201+
echo "=== Summary: $compliant/$total repos compliant ==="
202+
;;
203+
*)
204+
echo "Unknown action: $ACTION"
205+
echo "Usage: $0 {audit|setup|audit-all} OWNER/REPO_OR_OWNER"
206+
exit 1
207+
;;
208+
esac

0 commit comments

Comments
 (0)