Skip to content

feat: Implement BranchProtectionAssessor #86

@jeremyeder

Description

@jeremyeder

feat: Implement BranchProtectionAssessor

Attribute Definition

Attribute ID: branch_protection (Attribute #25 - Tier 3)

Definition: Required status checks and review approvals before merging to main/production branches.

Why It Matters: Prevents broken code from reaching production. Provides safety net for AI-generated code. Ensures quality gates are enforced.

Impact on Agent Behavior:

  • Understanding of merge requirements
  • Awareness of quality gates
  • Suggestions aligned with branch policies
  • Better PR creation (ensuring checks pass)

Measurable Criteria:

  • Branch protection enabled for main/master/production
  • Required status checks:
    • All tests passing
    • Linting/formatting passing
    • Code coverage threshold met
    • Security scanning passing
  • Required reviews: At least 1 approval
  • No force pushes to protected branches
  • No direct commits to protected branches
  • Up-to-date branch requirement (rebase/merge before merging)

Implementation Requirements

File Location: src/agentready/assessors/testing.py

Class Name: BranchProtectionAssessor

Tier: 3 (Important)

Default Weight: 0.015 (1.5% of total score)

Assessment Logic

Scoring Approach: Query GitHub API for branch protection rules

Evidence to Check (score components):

  1. Branch protection enabled (50%)

    • Use GitHub CLI or API to check protection rules
    • Check for main/master branch
  2. Required status checks (30%)

    • Verify status checks are required
    • Check for meaningful checks (not just empty list)
  3. Review requirements (20%)

    • Required approvals count
    • Dismiss stale reviews
    • Code owner reviews

Scoring Logic:

if branch_protection_enabled:
    protection_score = 50

    # Status checks
    if required_status_checks:
        status_score = 30
    else:
        status_score = 0

    # Reviews
    if required_approvals >= 1:
        review_score = 20
    else:
        review_score = 0

    total_score = protection_score + status_score + review_score
else:
    total_score = 0

status = "pass" if total_score >= 75 else "fail"

IMPORTANT: This assessor requires GitHub API access. If GitHub integration is not available, return not_applicable.

Code Pattern to Follow

Reference: New pattern - GitHub API integration

Pattern:

  1. Check if repository is GitHub-hosted (.git/config contains github.com)
  2. Use GitHub CLI (gh api) to query branch protection
  3. Parse JSON response for protection rules
  4. Calculate score based on protection level
  5. Handle API errors gracefully (return not_applicable if API unavailable)

Example Finding Responses

Pass (Score: 100)

Finding(
    attribute=self.attribute,
    status="pass",
    score=100.0,
    measured_value="full protection enabled",
    threshold="protection with checks and reviews",
    evidence=[
        "Branch protection enabled on 'main'",
        "Required status checks: 3 (tests, lint, security)",
        "Required reviews: 1 approval",
        "Force push disabled",
        "Direct commits disabled",
    ],
    remediation=None,
    error_message=None,
)

Fail (Score: 50)

Finding(
    attribute=self.attribute,
    status="fail",
    score=50.0,
    measured_value="basic protection only",
    threshold="protection with checks and reviews",
    evidence=[
        "Branch protection enabled on 'main'",
        "No required status checks configured",
        "No required reviews",
        "Force push disabled",
    ],
    remediation=self._create_remediation(),
    error_message=None,
)

Not Applicable

Finding.not_applicable(
    self.attribute,
    reason="Not a GitHub repository or GitHub API unavailable"
)

Registration

Add to src/agentready/services/scanner.py in create_all_assessors():

from ..assessors.testing import (
    TestCoverageAssessor,
    PreCommitHooksAssessor,
    TestNamingConventionsAssessor,
    CICDPipelineVisibilityAssessor,
    BranchProtectionAssessor,  # Add this import
)

def create_all_assessors() -> List[BaseAssessor]:
    return [
        # ... existing assessors ...
        BranchProtectionAssessor(),  # Add this line
    ]

Testing Guidance

Test File: tests/unit/test_assessors_testing.py

Test Cases:

  • SKIP unit tests for this assessor (requires GitHub API)
  • Integration tests would require test repositories
  • Mock GitHub API responses for basic validation

Note: This assessor is unique - it requires external API access. Consider making it optional or providing clear messaging when GitHub integration is unavailable.

Dependencies

External Tools:

  • GitHub CLI (gh) or GitHub API client
  • Requires authentication

Python Packages:

  • requests for GitHub API calls (optional)
  • Or use subprocess to call gh api

Installation Check:

def is_applicable(self, repository: Repository) -> bool:
    # Check if GitHub repo
    git_config = repository.path / ".git" / "config"
    if not git_config.exists():
        return False

    with open(git_config) as f:
        if "github.com" not in f.read():
            return False

    # Check if gh CLI available
    try:
        result = subprocess.run(
            ["gh", "--version"],
            capture_output=True,
            timeout=5,
        )
        return result.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False

Remediation Steps

def _create_remediation(self) -> Remediation:
    return Remediation(
        summary="Enable branch protection with required checks and reviews",
        steps=[
            "Go to GitHub repository settings",
            "Navigate to Branches > Branch protection rules",
            "Add rule for 'main' branch",
            "Enable: Require pull request reviews before merging",
            "Enable: Require status checks to pass",
            "Add status checks: tests, lint, coverage",
            "Disable: Allow force pushes",
            "Disable: Allow deletions",
        ],
        tools=["github-cli"],
        commands=[
            "# Enable branch protection via GitHub CLI",
            "gh api repos/:owner/:repo/branches/main/protection \\",
            "  --method PUT \\",
            "  --field required_status_checks='{'strict':true,'contexts':['test','lint']}' \\",
            "  --field required_pull_request_reviews='{'required_approving_review_count':1}' \\",
            "  --field enforce_admins=true \\",
            "  --field restrictions=null",
            "",
            "# Check current protection",
            "gh api repos/:owner/:repo/branches/main/protection",
        ],
        examples=[
            """# Example branch protection configuration (GitHub UI)

**Branch name pattern**: main

**Protect matching branches**:
- ✓ Require a pull request before merging
  - ✓ Require approvals: 1
  - ✓ Dismiss stale pull request approvals when new commits are pushed

- ✓ Require status checks to pass before merging
  - ✓ Require branches to be up to date before merging
  - Status checks:
    - test
    - lint
    - coverage
    - security-scan

- ✓ Do not allow bypassing the above settings
- ✓ Restrict who can push to matching branches

**Rules applied to everyone including administrators**:
- ✓ Block force pushes
- ✓ Do not allow deletions
""",
        ],
        citations=[
            Citation(
                source="GitHub Docs",
                title="About protected branches",
                url="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches",
                relevance="Official documentation on branch protection",
            ),
            Citation(
                source="GitHub Docs",
                title="Managing a branch protection rule",
                url="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
                relevance="Guide to configuring branch protection",
            ),
        ],
    )

Implementation Notes

  1. GitHub Detection: Parse .git/config to check for github.com remote
  2. API Authentication: Use gh CLI which handles auth automatically
  3. API Call: gh api repos/:owner/:repo/branches/main/protection
  4. Response Parsing: Parse JSON for protection rules
  5. Error Handling: Return not_applicable if:
    • Not a GitHub repo
    • GitHub CLI not installed
    • API call fails (permissions, network)
  6. Rate Limiting: GitHub API has rate limits, cache results if possible
  7. Alternative Approach: Check for .github/settings.yml (if using Settings app)

GitHub API Endpoint:

GET /repos/{owner}/{repo}/branches/{branch}/protection

Expected Response Structure:

{
  "required_status_checks": {
    "strict": true,
    "contexts": ["test", "lint", "coverage"]
  },
  "required_pull_request_reviews": {
    "required_approving_review_count": 1,
    "dismiss_stale_reviews": true
  },
  "enforce_admins": {
    "enabled": true
  }
}

Special Note: This is an advanced assessor that depends on external services. Consider making it optional or providing clear guidance when GitHub integration is unavailable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions