Skip to content

Commit 365a0c2

Browse files
yalemanclaude
andauthored
Branch protection rules support (#613)
* fixing some import naming * feat: add branch protection module with admin bypass and migration support - Fix allow_admin_bypass to correctly allow/prevent admin bypass (was inverted) - Add support for GitHub rulesets with admin bypass actors (role ID 5) - Implement migration from legacy branch protection to rulesets - Add separate check/fix for cleaning up legacy protection after migration - Update CLAUDE.md with corrected configuration documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * feat: add status check validation and fix ruleset API handling Added comprehensive status check validation to ensure required checks actually exist in workflow files before enforcing them in branch protection. Changes: - Add _get_available_checks_for_repo() to parse workflow YAML files and extract job names - Add _validate_required_checks() to compare required vs available checks with helpful warnings - Integrate validation into both check and fix functions - Fix _get_rulesets() to fetch full ruleset details instead of summaries (GitHub's list API returns rules: None and conditions: None) - Fix filter logic to handle conditions: None (applies to all branches) - Add debug logging for ruleset fetching and filtering - Add 9 comprehensive unit tests for validation logic Fixes: - Silent exit bug in fix_legacy_protection_cleanup (filter wasn't matching rulesets) - Missing PR rule detection (list API doesn't include full rule details) - Legacy protection cleanup now successfully removes old protection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 28be624 commit 365a0c2

File tree

8 files changed

+1457
-48
lines changed

8 files changed

+1457
-48
lines changed

CLAUDE.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
github_linter is a Python tool for auditing GitHub repositories at scale. It scans repositories for common configuration issues, missing files, and standardization opportunities across multiple repos.
8+
9+
## Development Commands
10+
11+
### Testing and Linting
12+
13+
- Run all precommit checks: `make precommit`
14+
- Run linting: `uv run ruff check github_linter tests`
15+
- Run type checking: `uv run mypy --strict github_linter tests`
16+
- Run tests: `uv run pytest github_linter tests`
17+
- Run single test: `uv run pytest tests/test_<module>.py::<test_name>`
18+
19+
### Running the CLI
20+
21+
- Run the CLI: `uv run python -m github_linter`
22+
- Run with filters: `uv run python -m github_linter --repo <repo_name> --owner <owner_name>`
23+
- Run specific module: `uv run python -m github_linter --module <module_name>`
24+
- Run with fixes: `uv run python -m github_linter --fix`
25+
- List available repos: `uv run python -m github_linter --list-repos`
26+
27+
### Web Interface
28+
29+
- Start web server: `uv run python -m github_linter.web`
30+
- Or use the script: `./run_web.sh`
31+
32+
### Docker
33+
34+
- Build container: `make docker_build`
35+
- Run web server in container: `make docker_run`
36+
37+
## Architecture
38+
39+
### Core Components
40+
41+
1. **GithubLinter** (`github_linter/__init__.py`) - Main orchestrator that:
42+
- Handles GitHub authentication (via environment variable `GITHUB_TOKEN` or config file)
43+
- Manages rate limiting
44+
- Coordinates module execution across repositories
45+
- Generates reports
46+
47+
2. **RepoLinter** (`github_linter/repolinter.py`) - Per-repository handler that:
48+
- Manages file caching for the repository
49+
- Runs test modules against the repository
50+
- Tracks errors, warnings, and fixes
51+
- Provides utility methods for checking files and languages
52+
- Handles file creation/updates with protected branch awareness
53+
54+
3. **Test Modules** (`github_linter/tests/`) - Pluggable modules that check specific aspects:
55+
- Each module must define `CATEGORY`, `LANGUAGES`, and `DEFAULT_CONFIG`
56+
- Functions starting with `check_` are automatically discovered and run
57+
- Functions starting with `fix_` are run when `--fix` flag is used
58+
- Modules are loaded dynamically in `tests/__init__.py`
59+
60+
### Available Test Modules
61+
62+
- `branch_protection` - Validates and configures branch protection on default branches
63+
- `codeowners` - Validates CODEOWNERS files
64+
- `dependabot` - Validates Dependabot configuration
65+
- `generic` - Checks for unwanted files, CODEOWNERS, FUNDING.yml
66+
- `github_actions` - Validates GitHub Actions workflows
67+
- `homebrew` - Homebrew-specific checks
68+
- `issues` - Reports on open issues and PRs
69+
- `mkdocs` - Ensures mkdocs projects have proper CI setup
70+
- `pyproject` - Validates pyproject.toml (authors, naming, configuration)
71+
- `security_md` - Checks for SECURITY.md
72+
- `terraform` - Checks Terraform provider configurations
73+
74+
### Module Language Filtering
75+
76+
Modules declare which languages they apply to via the `LANGUAGES` attribute:
77+
- Use `["all"]` for modules that apply to all repositories
78+
- Use specific languages (e.g., `["python"]`, `["rust"]`) to run only on repos with those languages
79+
- Language detection is based on GitHub's automatic language detection
80+
81+
### Configuration
82+
83+
Configuration file locations (in priority order):
84+
1. `./github_linter.json` (local directory)
85+
2. `~/.config/github_linter.json` (user config)
86+
87+
Each module can define `DEFAULT_CONFIG` which gets merged with user configuration.
88+
89+
#### Branch Protection Configuration
90+
91+
The `branch_protection` module supports both legacy branch protection rules and modern GitHub rulesets. It can check existing protection, create new protection, and migrate from legacy rules to rulesets.
92+
93+
```json
94+
{
95+
"branch_protection": {
96+
"enable_protection": true,
97+
"allow_admin_bypass": true,
98+
"require_pull_request": true,
99+
"required_approving_review_count": 1,
100+
"dismiss_stale_reviews": true,
101+
"require_code_owner_review": false,
102+
"use_rulesets": true,
103+
"migrate_to_rulesets": false,
104+
"warn_on_mismatch": true,
105+
"language_checks": {
106+
"Python": ["pytest", "ruff", "mypy"],
107+
"Rust": ["cargo-test", "clippy"],
108+
"JavaScript": ["test", "lint"],
109+
"TypeScript": ["test", "lint"],
110+
"Shell": ["shellcheck"],
111+
"Go": ["test", "lint"]
112+
}
113+
}
114+
}
115+
```
116+
117+
**Configuration Options:**
118+
- `enable_protection` - Whether to enable branch protection checks (default: true)
119+
- `allow_admin_bypass` - Allow repository admins to bypass protection requirements (default: true). For legacy protection, this sets `enforce_admins=false`. For rulesets, this adds repository admin role (ID 5) to `bypass_actors`.
120+
- `require_pull_request` - Require pull request before merging (default: true)
121+
- `required_approving_review_count` - Number of required PR approvals (default: 1)
122+
- `dismiss_stale_reviews` - Dismiss stale reviews when new commits are pushed (default: true)
123+
- `require_code_owner_review` - Require review from code owners (default: false)
124+
- `use_rulesets` - Prefer GitHub rulesets over legacy branch protection (default: true)
125+
- `migrate_to_rulesets` - Automatically migrate from legacy protection to rulesets when fixing (default: false)
126+
- `warn_on_mismatch` - If protection exists but doesn't match config, warn instead of error (default: true)
127+
- `language_checks` - Map of GitHub language names to required status check names. The module automatically determines which checks to require based on detected repository languages.
128+
129+
**Legacy vs Rulesets:**
130+
- Legacy branch protection: Traditional branch protection API (one rule per branch)
131+
- Rulesets: Modern GitHub protection (multiple rulesets aggregate, more features)
132+
- The module detects which system is in use and can work with both
133+
- With `use_rulesets: true`, new protection is created as rulesets
134+
- With `migrate_to_rulesets: true`, the fix function will convert legacy protection to rulesets
135+
- Both systems can coexist; the module checks both and reports on mismatches
136+
137+
**Implementation Notes:**
138+
- Uses PyGithub's `_requester` API for rulesets since PyGithub doesn't natively support them yet
139+
- Rulesets API requires GitHub API version 2022-11-28
140+
- Rulesets provide more granular control and organization-wide enforcement
141+
142+
### Exception Handling
143+
144+
The codebase uses custom exceptions for flow control:
145+
- `SkipOnArchived` - Skip check for archived repositories
146+
- `SkipOnPrivate` - Skip check for private repositories
147+
- `SkipOnPublic` - Skip check for public repositories
148+
- `SkipOnProtected` - Skip check when default branch is protected
149+
- `SkipNoLanguage` - Skip check when required language not present
150+
- `NoChangeNeeded` - Indicates no action needed
151+
152+
These exceptions are caught in `RepoLinter.run_module()` and suppress the check/fix function gracefully.
153+
154+
## Adding New Test Modules
155+
156+
1. Create a new module under `github_linter/tests/`
157+
2. Define required module-level attributes:
158+
- `CATEGORY: str` - Display name for reports
159+
- `LANGUAGES: List[str]` - Languages this module applies to (or `["all"]`)
160+
- `DEFAULT_CONFIG: Dict[str, Any]` - Default configuration
161+
3. Implement check functions: `def check_<something>(repo: RepoLinter) -> None`
162+
4. Implement fix functions: `def fix_<something>(repo: RepoLinter) -> None`
163+
5. Import the module in `github_linter/tests/__init__.py`
164+
6. Use `repo.error()`, `repo.warning()`, or `repo.fix()` to report results
165+
166+
## Testing Strategy
167+
168+
- Unit tests are in `/tests/` directory
169+
- Tests requiring network/authentication are marked with `@pytest.mark.network`
170+
- Run tests without network: `uv run pytest -m "not network"`
171+
- The codebase uses both PyGithub and github3.py libraries
172+
173+
## Code Style
174+
175+
- Line length: 200 characters (configured in pyproject.toml)
176+
- Type checking: strict mypy mode
177+
- Linting: ruff with pylint-pydantic plugin
178+
- Use pydantic for validation where appropriate
179+
- Use loguru for logging
180+
181+
## Dependencies
182+
183+
- Main GitHub libraries: `pygithub`, `github3-py`
184+
- Web framework: `fastapi`, `uvicorn`
185+
- Configuration: `json5`, `pyyaml`, `tomli`, `python-hcl2`
186+
- Type checking: `mypy` with strict mode
187+
- Testing: `pytest`

github_linter/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import click
66
from loguru import logger
77

8-
from . import GithubLinter, search_repos
9-
from .utils import setup_logging
10-
from .tests import MODULES, load_modules
8+
from github_linter import GithubLinter, search_repos
9+
from github_linter.utils import setup_logging
10+
from github_linter.tests import MODULES, load_modules
1111

1212
MODULE_CHOICES = [key for key in list(MODULES.keys()) if not key.startswith("github_linter")]
1313

github_linter/repolinter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
SkipOnPublic,
2626
)
2727

28-
from .types import DICTLIST
28+
from .custom_types import DICTLIST
2929
from .utils import load_config
3030

3131

github_linter/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
from . import (
11+
branch_protection, # noqa: F401
1112
codeowners, # noqa: F401
1213
dependabot, # noqa: F401
1314
docs, # noqa: F401

0 commit comments

Comments
 (0)