-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Summary
Implement a custom review gating system that:
- Replaces GitHub's native "required reviews" with a custom
approval-gatecheck - Adds bors-style
@delegatesupport, allowing maintainers to delegate PR approval rights to non-maintainers for a specific PR
This enables "drive-by contributions" where trusted community members can effectively approve PRs on behalf of maintainers.
Motivation
GitHub's native branch protection has a fundamental limitation: reviews from non-maintainers don't count toward required review counts. There's no API to make a non-maintainer's approval "count."
This creates friction for:
- Drive-by contributions: A trusted community member reviews a PR thoroughly but their approval doesn't "count"
- Domain expertise: Someone with deep knowledge of a specific area (but not a maintainer) can't formally approve changes
- Reducing maintainer burden: Maintainers must personally approve every PR even when a trusted reviewer has already done thorough review
The bors @delegate pattern solves this: a maintainer vouches for a specific reviewer on a specific PR, temporarily granting them approval authority.
Proposed Behavior
Delegation Commands
| Command | Description |
|---|---|
/delegate+ |
Delegate approval rights to the PR author |
/delegate=@username |
Delegate approval rights to a specific user |
/delegate=@user1,@user2 |
Delegate approval rights to multiple users |
/undelegate |
Revoke delegation |
Workflow
- Maintainer delegates: Comments
/delegate=@contributor - Bot confirms: Replies with confirmation
- Delegated user approves: Submits a GitHub review with "Approve"
- Check passes: The
approval-gate(ororg-required-checks) status turns green - Merge proceeds: Standard GitHub merge once all checks pass
Key Properties
- Delegation is per-PR only - no permanent permission changes
- Only maintainers (write access) can delegate
- Delegation is revocable via
/undelegate - All actions are auditable via PR comment history
- Delegated users cannot further delegate
Technical Approach
Core Idea: Replace Native Reviews with Custom Check
Instead of relying on GitHub's "Require X approving reviews" ruleset, we:
- Disable (or set to 0) native required reviews in the org ruleset
- Add a custom
approval-gatestatus check that implements our own approval logic - Require this check to pass before merge
This gives us full control over what constitutes a valid approval.
Architecture: Org-wide vs Repo-specific Checks
┌─────────────────────────────────────────────────────────────┐
│ Org-wide Ruleset (bootc-dev) │
│ Requires BOTH checks on default branch: │
│ ├── "required-checks" (repo-specific CI) │
│ └── "org-required-checks" (org-wide policy) │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌──────────────────────────────┐
│ Repo: ci.yml │ │ Org: org-required-checks.yml│
│ (repo-specific) │ │ (synced via sync-common) │
│ │ │ │
│ on: pull_request │ │ on: │
│ │ │ pull_request │
│ Jobs: │ │ pull_request_review │
│ - build │ │ issue_comment │
│ - test │ │ │
│ - lint │ │ Jobs: │
│ - required-checks │ │ - approval-gate │
│ (sentinel) │ │ - (other org checks) │
│ │ │ - org-required-checks │
│ │ │ (sentinel) │
└─────────────────────────┘ └──────────────────────────────┘
Why This Architecture?
Separation of concerns:
required-checks= repo-specific (build, test, lint - varies per repo)org-required-checks= org-wide policy (same for all repos)
Benefits:
- Adding org-wide checks doesn't touch individual repos
- Repos focus on their own CI
- Single source of truth for org policy in
bootc-dev/infra - Automatic sync via existing
sync-commonmechanism
Avoids re-running CI:
ci.ymlonly triggers onpull_request(code changes)org-required-checks.ymltriggers on comments/reviews too, but only runs lightweight approval check
Future extensibility: The org-required-checks workflow can incorporate other org-wide checks (OpenSSF scorecard, DCO, license checks, etc.) into the same sentinel pattern.
Implementation Components
1. Org-wide Ruleset
Configure at org level (Settings → Repository → Rulesets):
- Target: All repositories
- Target branches: Default branch
- Rule: Require status checks to pass
required-checks(repo-specific CI sentinel)org-required-checks(org-wide policy sentinel)
- Rule: Required reviews = 0 (or disable - we handle this ourselves)
2. Org-wide Workflow: org-required-checks.yml
# common/.github/workflows/org-required-checks.yml
name: Org Required Checks
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted, dismissed]
issue_comment:
types: [created, edited, deleted]
jobs:
approval-gate:
runs-on: ubuntu-latest
# Only run on PRs (issue_comment fires for issues too)
if: github.event.pull_request || github.event.issue.pull_request
steps:
- uses: bootc-dev/infra/actions/approval-gate@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Future: other org-wide checks can be added here
# Sentinel job
org-required-checks:
if: always()
needs: [approval-gate]
runs-on: ubuntu-latest
steps:
- run: exit 1
if: needs.approval-gate.result != 'success'3. Reusable Action: bootc-dev/infra/actions/approval-gate
# actions/approval-gate/action.yml
name: 'Approval Gate'
description: 'Check for maintainer approval or valid delegation'
inputs:
token:
description: 'GitHub token'
required: true
outputs:
approved:
description: 'Whether the PR is approved'
approved-by:
description: 'Username who approved'
delegated-by:
description: 'Maintainer who delegated (if applicable)'
runs:
using: 'composite'
steps:
- name: Check approval status
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Get collaborators with write access (maintainers)
MAINTAINERS=$(gh api "repos/$REPO/collaborators?permission=push" --jq '.[].login')
# Get PR reviews
APPROVALS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
--jq '[.[] | select(.state == "APPROVED")] | .[].user.login')
# Check if any maintainer approved
for approver in $APPROVALS; do
if echo "$MAINTAINERS" | grep -qx "$approver"; then
echo "Approved by maintainer: $approver"
exit 0
fi
done
# Check for delegation
DELEGATE_CMD=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '[.[] | select(.body | test("@delegate[+=]"))] | last')
if [ -n "$DELEGATE_CMD" ]; then
DELEGATOR=$(echo "$DELEGATE_CMD" | jq -r '.user.login')
# Verify delegator is a maintainer
if echo "$MAINTAINERS" | grep -qx "$DELEGATOR"; then
# Parse delegatee from command
DELEGATEE=$(echo "$DELEGATE_CMD" | jq -r '.body' | \
grep -oP '@delegate[+=]@?\K[\w-]+')
# Check if delegatee approved
if echo "$APPROVALS" | grep -qx "$DELEGATEE"; then
echo "Approved by delegatee: $DELEGATEE (delegated by $DELEGATOR)"
exit 0
fi
fi
fi
echo "No valid approval found"
exit 1Detecting Maintainers
Use the Collaborators API to check for users with push (write) or admin permission:
gh api "repos/$REPO/collaborators?permission=push" --jq '.[].login'This matches GitHub's native permission model and requires no configuration.
State Management
No external state needed - the action parses PR state on each run:
- Fetch collaborators with write access (maintainers)
- Fetch all PR reviews
- Check if any maintainer approved → pass
- Parse comments for
@delegatecommands from maintainers - Check if delegatee approved → pass
- Otherwise → fail
Stateless and idempotent.
Event Handling
| Event | What triggers it | Action behavior |
|---|---|---|
pull_request.opened |
PR created | Check for approvals (usually none) |
pull_request.synchronize |
New commits pushed | Re-check (may need re-approval) |
pull_request_review.submitted |
Review added | Check if approver is maintainer or delegatee |
pull_request_review.dismissed |
Review removed | Re-check remaining approvals |
issue_comment.created |
Comment added | Parse for @delegate commands |
issue_comment.edited |
Comment edited | Re-parse |
issue_comment.deleted |
Comment deleted | Re-check (delegation may be revoked) |
Rollout Plan
- Phase 1: Create
approval-gateaction inbootc-dev/infra/actions/ - Phase 2: Create
org-required-checks.ymlworkflow incommon/.github/workflows/ - Phase 3: Test in
bootc-dev/infrarepo - Phase 4: Sync to all repos via
sync-common - Phase 5: Create org-wide ruleset requiring both
required-checksandorg-required-checks - Phase 6: Disable native "required reviews" in org ruleset (our check handles it)
- Phase 7: Document usage
Security Considerations
- Trust model: Maintainers must trust delegated users to responsibly approve
- Audit trail: All
@delegatecommands visible in PR comment history - No privilege escalation: Delegated users can only approve the specific PR
- Revocable:
@undelegateor deleting the comment removes delegation - No bypass: Works within GitHub's native protection system (status checks)
- Commit invalidation: New commits re-trigger the check (stale approvals may need re-review)
Alternatives Considered
- Keep native reviews + add delegation as extra check: Doesn't solve the core problem - non-maintainer reviews still don't count
- Bypass-actor merge bot: More powerful but bypasses GitHub's security model entirely
- Add trusted contributors as collaborators: Grants too much permanent access
- Third-party tools (Mergify/Kodiak): Can't implement custom delegation logic
Open Questions
- Should delegation auto-expire on new commits pushed to the PR?
- Implementation language: Composite action (bash/gh CLI) vs Node.js vs Rust?
- How to handle CODEOWNERS - does delegatee approval satisfy code owner requirements?
- Should the action post a comment confirming delegation?
Future Work
The org-required-checks workflow provides a natural place to add other org-wide checks:
- OpenSSF scorecard gate (consolidate existing
openssf-scorecard-gate.yml) - DCO (Developer Certificate of Origin) check
- License compliance check
- etc.
These would be added as additional jobs in the workflow, with the sentinel job aggregating them.
Prior Art
- bors-ng: https://github.com/bors-ng/bors-ng - delegation model inspiration
- homu: https://github.com/rust-lang/homu - rust-lang's original bors implementation
- bootc-dev/bootc ci.yml: Sentinel job pattern for
required-checks - GitHub org rulesets: Native support for org-wide required checks
Related Work
- Fits alongside existing
rebase.ymlworkflow pattern - Synced via existing
sync-commonmechanism - Uses existing
bootc-dev Botidentity for comments (if we add confirmation replies)
toolbx(rhel-toolbox-stream10) ~/src/github/bootc-dev/infra> opencode -c
^[[<51;244;38M
toolbx(rhel-toolbox-stream10) ~/src/github/bootc-dev/infra> cat /tmp/review.md
Custom review gating with @delegate support
Summary
Enable maintainers to temporarily empower non-maintainers to approve specific PRs, following rust-lang/bors's delegation model.
GitHub's native required reviews only counts approvals from users with write access. We replace this with a custom approval-gate status check implementing our own approval logic, including delegation.
Commands
@delegate+- delegate to the PR author@delegate=@useror@delegate=@user1,@user2- delegate to specific user(s)@undelegate- revoke
Architecture
flowchart TB
subgraph ruleset["Org-wide Ruleset"]
rc["required-checks<br/>(repo-specific CI)"]
orc["org-required-checks<br/>(approval-gate + future checks)"]
end
subgraph repo["Each Repo"]
ci["ci.yml<br/>on: pull_request"]
org["org-required-checks.yml<br/>on: pull_request, review, comment"]
end
ci --> rc
org --> orc
org-required-checks.yml lives in common/.github/workflows/ and syncs to all repos. Triggers on pull_request, pull_request_review, and issue_comment to run the approval check.
Workflow
# common/.github/workflows/org-required-checks.yml
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted, dismissed]
issue_comment:
types: [created, edited, deleted]
jobs:
approval-gate:
runs-on: ubuntu-latest
if: github.event.pull_request || github.event.issue.pull_request
steps:
- uses: bootc-dev/infra/actions/approval-gate@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
org-required-checks:
if: always()
needs: [approval-gate]
runs-on: ubuntu-latest
steps:
- run: exit 1
if: needs.approval-gate.result != 'success'The approval-gate action
Stateless - parses PR state on each run:
- Fetch collaborators with write access (maintainers)
- Fetch PR reviews
- Pass if maintainer approved OR (maintainer delegated AND delegatee approved)
Maintainers detected via collaborators API (permission=push).
Rollout
- Create
approval-gateaction inbootc-dev/infra/actions/ - Create
org-required-checks.ymlincommon/.github/workflows/ - Test in bootc-dev/infra, then sync to all repos
- Configure org-wide ruleset requiring both checks; disable native required reviews
Open questions
- Command syntax:
@delegate=@uservs@bot delegate=@uservs/delegate @user? - Should delegation expire on new commits?
- Implementation: composite action (bash) vs Node.js vs Rust?