codeowners-coverage 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeowners_coverage-0.1.0/PKG-INFO +158 -0
- codeowners_coverage-0.1.0/README.md +141 -0
- codeowners_coverage-0.1.0/pyproject.toml +41 -0
- codeowners_coverage-0.1.0/setup.cfg +4 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/__init__.py +9 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/__main__.py +12 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/checker.py +157 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/cli.py +130 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/config.py +77 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage/matcher.py +88 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/PKG-INFO +158 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/SOURCES.txt +17 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/dependency_links.txt +1 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/entry_points.txt +2 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/requires.txt +9 -0
- codeowners_coverage-0.1.0/src/codeowners_coverage.egg-info/top_level.txt +1 -0
- codeowners_coverage-0.1.0/tests/test_checker.py +214 -0
- codeowners_coverage-0.1.0/tests/test_config.py +106 -0
- codeowners_coverage-0.1.0/tests/test_matcher.py +173 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeowners-coverage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Measure and enforce CODEOWNERS coverage
|
|
5
|
+
Author-email: Sentry Team <hello@sentry.io>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pathspec>=0.12.0
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Requires-Dist: click>=8.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
15
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# CODEOWNERS Coverage
|
|
19
|
+
|
|
20
|
+
A tool to measure and enforce CODEOWNERS coverage in your repository.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install codeowners-coverage
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Check coverage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
codeowners-coverage check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Generate baseline
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
codeowners-coverage baseline
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Configuration
|
|
43
|
+
|
|
44
|
+
Create a `.codeowners-config.yml` file in your repository root:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
# Path to CODEOWNERS file
|
|
48
|
+
codeowners_path: ".github/CODEOWNERS"
|
|
49
|
+
|
|
50
|
+
# Path to baseline file
|
|
51
|
+
baseline_path: ".github/codeowners-coverage-baseline.txt"
|
|
52
|
+
|
|
53
|
+
# File patterns to exclude from coverage checking
|
|
54
|
+
exclusions:
|
|
55
|
+
- "**/__pycache__/**"
|
|
56
|
+
- "**/*.pyc"
|
|
57
|
+
- "node_modules/**"
|
|
58
|
+
- "dist/**"
|
|
59
|
+
- ".venv/**"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## GitHub Actions Integration
|
|
63
|
+
|
|
64
|
+
Add a workflow to check CODEOWNERS coverage on pull requests:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
name: CODEOWNERS Coverage
|
|
68
|
+
on: [pull_request]
|
|
69
|
+
|
|
70
|
+
jobs:
|
|
71
|
+
check:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- uses: actions/setup-python@v5
|
|
76
|
+
with:
|
|
77
|
+
python-version: '3.11'
|
|
78
|
+
- run: pip install codeowners-coverage
|
|
79
|
+
- run: codeowners-coverage check
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
- **Pattern Matching**: Supports all CODEOWNERS patterns (wildcards, globstars, etc.)
|
|
85
|
+
- **Baseline Support**: Track progress incrementally with a baseline allowlist
|
|
86
|
+
- **Configurable**: Customize exclusions and paths via YAML config
|
|
87
|
+
- **Fast**: Uses `pathspec` library for efficient pattern matching
|
|
88
|
+
- **GitHub Actions Ready**: Easy integration with CI/CD pipelines
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
### Using just (recommended)
|
|
93
|
+
|
|
94
|
+
This project includes a `justfile` with common development commands:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Install package in development mode
|
|
98
|
+
just install
|
|
99
|
+
|
|
100
|
+
# Run all tests
|
|
101
|
+
just test
|
|
102
|
+
|
|
103
|
+
# Run tests with coverage report
|
|
104
|
+
just test-cov
|
|
105
|
+
|
|
106
|
+
# Run type checking
|
|
107
|
+
just typecheck
|
|
108
|
+
|
|
109
|
+
# Run linting
|
|
110
|
+
just lint
|
|
111
|
+
|
|
112
|
+
# Run all checks (tests, typecheck, lint)
|
|
113
|
+
just check
|
|
114
|
+
|
|
115
|
+
# Clean build artifacts
|
|
116
|
+
just clean
|
|
117
|
+
|
|
118
|
+
# Build the package
|
|
119
|
+
just build
|
|
120
|
+
|
|
121
|
+
# Or build with uv (faster)
|
|
122
|
+
just build-uv
|
|
123
|
+
|
|
124
|
+
# Format code
|
|
125
|
+
just format
|
|
126
|
+
|
|
127
|
+
# Publish to Test PyPI
|
|
128
|
+
just publish-test
|
|
129
|
+
|
|
130
|
+
# Publish to production PyPI
|
|
131
|
+
just publish
|
|
132
|
+
|
|
133
|
+
# Full release workflow
|
|
134
|
+
just release
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Manual commands
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Install in development mode
|
|
141
|
+
uv pip install -e ".[dev]"
|
|
142
|
+
|
|
143
|
+
# Run tests
|
|
144
|
+
pytest tests/ -v
|
|
145
|
+
|
|
146
|
+
# Run with coverage
|
|
147
|
+
pytest tests/ -v --cov
|
|
148
|
+
|
|
149
|
+
# Build package
|
|
150
|
+
python -m build
|
|
151
|
+
|
|
152
|
+
# Publish to PyPI
|
|
153
|
+
twine upload dist/*
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
Apache-2.0
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# CODEOWNERS Coverage
|
|
2
|
+
|
|
3
|
+
A tool to measure and enforce CODEOWNERS coverage in your repository.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install codeowners-coverage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Check coverage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
codeowners-coverage check
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Generate baseline
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
codeowners-coverage baseline
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Configuration
|
|
26
|
+
|
|
27
|
+
Create a `.codeowners-config.yml` file in your repository root:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
# Path to CODEOWNERS file
|
|
31
|
+
codeowners_path: ".github/CODEOWNERS"
|
|
32
|
+
|
|
33
|
+
# Path to baseline file
|
|
34
|
+
baseline_path: ".github/codeowners-coverage-baseline.txt"
|
|
35
|
+
|
|
36
|
+
# File patterns to exclude from coverage checking
|
|
37
|
+
exclusions:
|
|
38
|
+
- "**/__pycache__/**"
|
|
39
|
+
- "**/*.pyc"
|
|
40
|
+
- "node_modules/**"
|
|
41
|
+
- "dist/**"
|
|
42
|
+
- ".venv/**"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## GitHub Actions Integration
|
|
46
|
+
|
|
47
|
+
Add a workflow to check CODEOWNERS coverage on pull requests:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
name: CODEOWNERS Coverage
|
|
51
|
+
on: [pull_request]
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
check:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
- uses: actions/setup-python@v5
|
|
59
|
+
with:
|
|
60
|
+
python-version: '3.11'
|
|
61
|
+
- run: pip install codeowners-coverage
|
|
62
|
+
- run: codeowners-coverage check
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- **Pattern Matching**: Supports all CODEOWNERS patterns (wildcards, globstars, etc.)
|
|
68
|
+
- **Baseline Support**: Track progress incrementally with a baseline allowlist
|
|
69
|
+
- **Configurable**: Customize exclusions and paths via YAML config
|
|
70
|
+
- **Fast**: Uses `pathspec` library for efficient pattern matching
|
|
71
|
+
- **GitHub Actions Ready**: Easy integration with CI/CD pipelines
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
### Using just (recommended)
|
|
76
|
+
|
|
77
|
+
This project includes a `justfile` with common development commands:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Install package in development mode
|
|
81
|
+
just install
|
|
82
|
+
|
|
83
|
+
# Run all tests
|
|
84
|
+
just test
|
|
85
|
+
|
|
86
|
+
# Run tests with coverage report
|
|
87
|
+
just test-cov
|
|
88
|
+
|
|
89
|
+
# Run type checking
|
|
90
|
+
just typecheck
|
|
91
|
+
|
|
92
|
+
# Run linting
|
|
93
|
+
just lint
|
|
94
|
+
|
|
95
|
+
# Run all checks (tests, typecheck, lint)
|
|
96
|
+
just check
|
|
97
|
+
|
|
98
|
+
# Clean build artifacts
|
|
99
|
+
just clean
|
|
100
|
+
|
|
101
|
+
# Build the package
|
|
102
|
+
just build
|
|
103
|
+
|
|
104
|
+
# Or build with uv (faster)
|
|
105
|
+
just build-uv
|
|
106
|
+
|
|
107
|
+
# Format code
|
|
108
|
+
just format
|
|
109
|
+
|
|
110
|
+
# Publish to Test PyPI
|
|
111
|
+
just publish-test
|
|
112
|
+
|
|
113
|
+
# Publish to production PyPI
|
|
114
|
+
just publish
|
|
115
|
+
|
|
116
|
+
# Full release workflow
|
|
117
|
+
just release
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Manual commands
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Install in development mode
|
|
124
|
+
uv pip install -e ".[dev]"
|
|
125
|
+
|
|
126
|
+
# Run tests
|
|
127
|
+
pytest tests/ -v
|
|
128
|
+
|
|
129
|
+
# Run with coverage
|
|
130
|
+
pytest tests/ -v --cov
|
|
131
|
+
|
|
132
|
+
# Build package
|
|
133
|
+
python -m build
|
|
134
|
+
|
|
135
|
+
# Publish to PyPI
|
|
136
|
+
twine upload dist/*
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
Apache-2.0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codeowners-coverage"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Measure and enforce CODEOWNERS coverage"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sentry Team", email = "hello@sentry.io" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pathspec>=0.12.0", # GitIgnore pattern matching
|
|
17
|
+
"PyYAML>=6.0", # Config file parsing
|
|
18
|
+
"click>=8.0", # CLI framework
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
codeowners-coverage = "codeowners_coverage.__main__:main"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7.0",
|
|
27
|
+
"pytest-cov>=4.0",
|
|
28
|
+
"mypy>=1.0",
|
|
29
|
+
"ruff>=0.1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 100
|
|
34
|
+
target-version = "py39"
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
python_version = "3.9"
|
|
38
|
+
strict = true
|
|
39
|
+
warn_return_any = true
|
|
40
|
+
warn_unused_configs = true
|
|
41
|
+
disallow_untyped_defs = true
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Core coverage checking logic for CODEOWNERS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Set
|
|
8
|
+
|
|
9
|
+
import pathspec
|
|
10
|
+
|
|
11
|
+
from .config import Config
|
|
12
|
+
from .matcher import CodeOwnersPatternMatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CoverageChecker:
|
|
16
|
+
"""Check CODEOWNERS coverage for repository files."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: Config) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initialize coverage checker.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Configuration object
|
|
24
|
+
"""
|
|
25
|
+
self.config = config
|
|
26
|
+
self.matcher = CodeOwnersPatternMatcher(config.codeowners_path)
|
|
27
|
+
self.exclusions = pathspec.PathSpec.from_lines("gitignore", config.exclusions)
|
|
28
|
+
|
|
29
|
+
def get_repository_files(self) -> List[str]:
|
|
30
|
+
"""
|
|
31
|
+
Get all files in repository using git ls-files.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of file paths relative to repository root
|
|
35
|
+
"""
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["git", "ls-files"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
files = result.stdout.strip().split("\n")
|
|
43
|
+
# Filter out empty strings
|
|
44
|
+
return [f for f in files if f]
|
|
45
|
+
|
|
46
|
+
def _load_baseline(self) -> Set[str]:
|
|
47
|
+
"""
|
|
48
|
+
Load baseline file of allowed uncovered files.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Set of file paths that are allowed to be uncovered
|
|
52
|
+
"""
|
|
53
|
+
baseline_path = Path(self.config.baseline_path)
|
|
54
|
+
|
|
55
|
+
if not baseline_path.exists():
|
|
56
|
+
return set()
|
|
57
|
+
|
|
58
|
+
baseline_files = set()
|
|
59
|
+
with open(baseline_path) as f:
|
|
60
|
+
for line in f:
|
|
61
|
+
line = line.strip()
|
|
62
|
+
# Skip comments and empty lines
|
|
63
|
+
if line and not line.startswith("#"):
|
|
64
|
+
baseline_files.add(line)
|
|
65
|
+
|
|
66
|
+
return baseline_files
|
|
67
|
+
|
|
68
|
+
def check_coverage(self, files: List[str] | None = None) -> Dict[str, object]:
|
|
69
|
+
"""
|
|
70
|
+
Check which files lack CODEOWNERS coverage.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
files: Optional list of specific files to check.
|
|
74
|
+
If None, checks all repository files.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dictionary with coverage statistics:
|
|
78
|
+
{
|
|
79
|
+
"total_files": int,
|
|
80
|
+
"covered_files": int,
|
|
81
|
+
"uncovered_files": [list of uncovered file paths],
|
|
82
|
+
"baseline_files": [list of baseline file paths],
|
|
83
|
+
"coverage_percentage": float
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
if files is None:
|
|
87
|
+
files = self.get_repository_files()
|
|
88
|
+
|
|
89
|
+
# Filter excluded files
|
|
90
|
+
filtered_files = [f for f in files if not self.exclusions.match_file(f)]
|
|
91
|
+
|
|
92
|
+
# Check coverage
|
|
93
|
+
uncovered = [f for f in filtered_files if not self.matcher.matches(f)]
|
|
94
|
+
|
|
95
|
+
# Load baseline
|
|
96
|
+
baseline = self._load_baseline()
|
|
97
|
+
|
|
98
|
+
# Separate new uncovered files from baseline files
|
|
99
|
+
new_uncovered = [f for f in uncovered if f not in baseline]
|
|
100
|
+
baseline_files = [f for f in uncovered if f in baseline]
|
|
101
|
+
|
|
102
|
+
# Calculate coverage
|
|
103
|
+
total_files = len(filtered_files)
|
|
104
|
+
covered_files = total_files - len(uncovered)
|
|
105
|
+
coverage_percentage = (covered_files / total_files * 100) if total_files > 0 else 100.0
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"total_files": total_files,
|
|
109
|
+
"covered_files": covered_files,
|
|
110
|
+
"uncovered_files": new_uncovered,
|
|
111
|
+
"baseline_files": baseline_files,
|
|
112
|
+
"coverage_percentage": coverage_percentage,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def generate_baseline(self, files: List[str] | None = None) -> List[str]:
|
|
116
|
+
"""
|
|
117
|
+
Generate a baseline of all currently uncovered files.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
files: Optional list of specific files to check.
|
|
121
|
+
If None, checks all repository files.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of uncovered file paths (sorted)
|
|
125
|
+
"""
|
|
126
|
+
if files is None:
|
|
127
|
+
files = self.get_repository_files()
|
|
128
|
+
|
|
129
|
+
# Filter excluded files
|
|
130
|
+
filtered_files = [f for f in files if not self.exclusions.match_file(f)]
|
|
131
|
+
|
|
132
|
+
# Check coverage
|
|
133
|
+
uncovered = [f for f in filtered_files if not self.matcher.matches(f)]
|
|
134
|
+
|
|
135
|
+
return sorted(uncovered)
|
|
136
|
+
|
|
137
|
+
def write_baseline(self, baseline_files: List[str]) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Write baseline file.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
baseline_files: List of file paths to write to baseline
|
|
143
|
+
"""
|
|
144
|
+
baseline_path = Path(self.config.baseline_path)
|
|
145
|
+
baseline_path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
with open(baseline_path, "w") as f:
|
|
148
|
+
f.write("# CODEOWNERS Coverage Baseline\n")
|
|
149
|
+
f.write("# Files lacking CODEOWNERS coverage (sorted)\n")
|
|
150
|
+
f.write("# Goal: Reduce this list to zero\n")
|
|
151
|
+
f.write("#\n")
|
|
152
|
+
f.write("# Generated by: codeowners-coverage baseline\n")
|
|
153
|
+
f.write("#\n")
|
|
154
|
+
f.write("\n")
|
|
155
|
+
|
|
156
|
+
for filepath in baseline_files:
|
|
157
|
+
f.write(f"{filepath}\n")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""CLI interface for codeowners-coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from .checker import CoverageChecker
|
|
12
|
+
from .config import Config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.version_option(version="0.1.0")
|
|
17
|
+
def cli() -> None:
|
|
18
|
+
"""CODEOWNERS coverage checking tool."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command()
|
|
23
|
+
@click.option("--json", "output_json", is_flag=True, help="Output JSON format")
|
|
24
|
+
@click.option("--files", multiple=True, help="Specific files to check")
|
|
25
|
+
@click.option("--config", default=".codeowners-config.yml", help="Config file path")
|
|
26
|
+
def check(output_json: bool, files: Tuple[str, ...], config: str) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Check CODEOWNERS coverage.
|
|
29
|
+
|
|
30
|
+
Validates that all files in the repository have CODEOWNERS coverage.
|
|
31
|
+
Files in the baseline are allowed to be uncovered.
|
|
32
|
+
New uncovered files will cause the check to fail.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
cfg = Config.load(config)
|
|
36
|
+
checker = CoverageChecker(cfg)
|
|
37
|
+
|
|
38
|
+
file_list: List[str] | None = list(files) if files else None
|
|
39
|
+
result = checker.check_coverage(file_list)
|
|
40
|
+
|
|
41
|
+
if output_json:
|
|
42
|
+
click.echo(json.dumps(result, indent=2))
|
|
43
|
+
else:
|
|
44
|
+
_print_human_readable_result(result)
|
|
45
|
+
|
|
46
|
+
# Exit with error if there are new uncovered files
|
|
47
|
+
if result["uncovered_files"]:
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
# Exit with code 2 if baseline can be reduced (positive signal)
|
|
51
|
+
# Check if any baseline files are now covered
|
|
52
|
+
baseline_set = set(result["baseline_files"])
|
|
53
|
+
current_uncovered = checker.generate_baseline(file_list)
|
|
54
|
+
current_uncovered_set = set(current_uncovered)
|
|
55
|
+
|
|
56
|
+
if len(baseline_set - current_uncovered_set) > 0:
|
|
57
|
+
if not output_json:
|
|
58
|
+
removable_files = sorted(baseline_set - current_uncovered_set)
|
|
59
|
+
click.echo(f"\nš Great news! {len(removable_files)} files can be removed from the baseline:")
|
|
60
|
+
for f in removable_files[:10]: # Show first 10
|
|
61
|
+
click.echo(f" - {f}")
|
|
62
|
+
if len(removable_files) > 10:
|
|
63
|
+
click.echo(f" ... and {len(removable_files) - 10} more")
|
|
64
|
+
click.echo("\nThese files now have CODEOWNERS coverage! Update the baseline:")
|
|
65
|
+
click.echo(" codeowners-coverage baseline")
|
|
66
|
+
sys.exit(2)
|
|
67
|
+
|
|
68
|
+
except FileNotFoundError as e:
|
|
69
|
+
click.echo(f"ā Error: {e}", err=True)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
click.echo(f"ā Unexpected error: {e}", err=True)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@cli.command()
|
|
77
|
+
@click.option("--config", default=".codeowners-config.yml", help="Config file path")
|
|
78
|
+
@click.option("--files", multiple=True, help="Specific files to check")
|
|
79
|
+
def baseline(config: str, files: Tuple[str, ...]) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Generate or update baseline file of uncovered files.
|
|
82
|
+
|
|
83
|
+
Creates a baseline of all currently uncovered files. This allows
|
|
84
|
+
incremental improvement by preventing new uncovered files while
|
|
85
|
+
allowing existing gaps.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
cfg = Config.load(config)
|
|
89
|
+
checker = CoverageChecker(cfg)
|
|
90
|
+
|
|
91
|
+
file_list: List[str] | None = list(files) if files else None
|
|
92
|
+
baseline_files = checker.generate_baseline(file_list)
|
|
93
|
+
|
|
94
|
+
# Write baseline
|
|
95
|
+
checker.write_baseline(baseline_files)
|
|
96
|
+
|
|
97
|
+
click.echo(f"ā
Baseline written to {cfg.baseline_path}")
|
|
98
|
+
click.echo(f"š {len(baseline_files)} uncovered files in baseline")
|
|
99
|
+
|
|
100
|
+
if baseline_files:
|
|
101
|
+
click.echo(f"\nš” Goal: Reduce this list to zero by adding CODEOWNERS coverage")
|
|
102
|
+
|
|
103
|
+
except FileNotFoundError as e:
|
|
104
|
+
click.echo(f"ā Error: {e}", err=True)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
click.echo(f"ā Unexpected error: {e}", err=True)
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _print_human_readable_result(result: dict) -> None:
|
|
112
|
+
"""Print coverage check result in human-readable format."""
|
|
113
|
+
total = result["total_files"]
|
|
114
|
+
covered = result["covered_files"]
|
|
115
|
+
uncovered = result["uncovered_files"]
|
|
116
|
+
baseline = result["baseline_files"]
|
|
117
|
+
percentage = result["coverage_percentage"]
|
|
118
|
+
|
|
119
|
+
if uncovered:
|
|
120
|
+
click.echo("ā CODEOWNERS Coverage Check Failed\n")
|
|
121
|
+
click.echo(f"The following {len(uncovered)} files lack CODEOWNERS coverage:")
|
|
122
|
+
for f in uncovered:
|
|
123
|
+
click.echo(f" - {f}")
|
|
124
|
+
click.echo("\nPlease add these files to .github/CODEOWNERS with appropriate owners.")
|
|
125
|
+
click.echo("\nš” Need help? Check the team mapping in the CODEOWNERS file")
|
|
126
|
+
click.echo(f"\nš Current status: {len(baseline)} files in baseline (unchanged)")
|
|
127
|
+
else:
|
|
128
|
+
click.echo(f"ā
CODEOWNERS Coverage: {percentage:.1f}% ({covered}/{total} files covered)")
|
|
129
|
+
if baseline:
|
|
130
|
+
click.echo(f"\nš” Baseline: {len(baseline)} files still need coverage")
|