Public Access
feat: add refactoring label bypass for check_pr_size
PRs with the 'refactoring' label bypass the PR size check, allowing large but legitimate refactoring PRs. The check_pr_size CLI now accepts --repo and --pr-number to query PR labels via the Gitea API. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0de18ebb21
commit
53c4823e04
@@ -121,11 +121,14 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: ${{ env.PYTHONPATH }}
|
PYTHONPATH: ${{ env.PYTHONPATH }}
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.check_pr_size \
|
python3 -m devx.ci.check_pr_size \
|
||||||
--base "origin/master" \
|
--base "origin/master" \
|
||||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--pr-number "${{ github.event.number }}" \
|
||||||
--github-output
|
--github-output
|
||||||
# --- release-dry-run step (conditional) ---
|
# --- release-dry-run step (conditional) ---
|
||||||
- name: Release dry-run validation
|
- name: Release dry-run validation
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
Enforces max lines changed and max files changed to keep PRs small
|
Enforces max lines changed and max files changed to keep PRs small
|
||||||
and deployable. Generated/excluded files are not counted.
|
and deployable. Generated/excluded files are not counted.
|
||||||
|
|
||||||
|
PRs with the ``refactoring`` label bypass the size check — large but
|
||||||
|
legitimate refactoring PRs that touch many files in a coordinated way.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python -m devx.ci.check_pr_size --base origin/master --head HEAD
|
python -m devx.ci.check_pr_size --base origin/master --head HEAD
|
||||||
|
|
||||||
@@ -19,8 +22,11 @@ import subprocess # nosec B404
|
|||||||
import click
|
import click
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from devx.api_clients import GiteaClient
|
||||||
from devx.ci._shared import write_github_output
|
from devx.ci._shared import write_github_output
|
||||||
|
from devx.config import GITEA_API_URL
|
||||||
from devx.i18n import _
|
from devx.i18n import _
|
||||||
|
from devx.tokens import get_ci_token
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -40,6 +46,20 @@ DEFAULT_EXCLUDED_PATTERNS = [
|
|||||||
|
|
||||||
DEFAULT_MAX_LINES = 500
|
DEFAULT_MAX_LINES = 500
|
||||||
DEFAULT_MAX_FILES = 10
|
DEFAULT_MAX_FILES = 10
|
||||||
|
REFACTORING_LABEL = "refactoring"
|
||||||
|
|
||||||
|
|
||||||
|
def has_refactoring_label(repo: str, pr_number: int) -> bool:
|
||||||
|
"""Check if a PR has the 'refactoring' label (bypasses size check)."""
|
||||||
|
try:
|
||||||
|
token = get_ci_token()
|
||||||
|
owner, repo_name = repo.split("/", 1)
|
||||||
|
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
||||||
|
pr = client.get_pr(pr_number)
|
||||||
|
labels = pr.get("labels", [])
|
||||||
|
return any(label.get("name") == REFACTORING_LABEL for label in labels)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_diff_stats(base: str, head: str) -> list[tuple[str, int, int]]:
|
def get_diff_stats(base: str, head: str) -> list[tuple[str, int, int]]:
|
||||||
@@ -147,6 +167,8 @@ def check_size(
|
|||||||
multiple=True,
|
multiple=True,
|
||||||
help=_("Additional excluded patterns (in addition to defaults)"),
|
help=_("Additional excluded patterns (in addition to defaults)"),
|
||||||
)
|
)
|
||||||
|
@click.option("--repo", default=None, help=_("Repo (owner/name) for label check"))
|
||||||
|
@click.option("--pr-number", type=int, default=None, help=_("PR number for label check"))
|
||||||
def cli(
|
def cli(
|
||||||
base: str,
|
base: str,
|
||||||
head: str,
|
head: str,
|
||||||
@@ -154,8 +176,19 @@ def cli(
|
|||||||
max_files: int,
|
max_files: int,
|
||||||
github_output: bool,
|
github_output: bool,
|
||||||
excluded: tuple[str, ...],
|
excluded: tuple[str, ...],
|
||||||
|
repo: str | None,
|
||||||
|
pr_number: int | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check PR size and reject oversized PRs."""
|
"""Check PR size and reject oversized PRs."""
|
||||||
|
# Check for refactoring label bypass
|
||||||
|
if repo and pr_number and has_refactoring_label(repo, pr_number):
|
||||||
|
detail = _("PR has 'refactoring' label — size check bypassed.")
|
||||||
|
if github_output:
|
||||||
|
write_github_output("pr-size-ok", "true")
|
||||||
|
write_github_output("pr-size-detail", detail)
|
||||||
|
click.echo(f"[pr-size] {detail}")
|
||||||
|
return
|
||||||
|
|
||||||
excluded_patterns = list(DEFAULT_EXCLUDED_PATTERNS) + list(excluded)
|
excluded_patterns = list(DEFAULT_EXCLUDED_PATTERNS) + list(excluded)
|
||||||
stats = get_diff_stats(base, head)
|
stats = get_diff_stats(base, head)
|
||||||
is_ok, detail = check_size(stats, max_lines, max_files, excluded_patterns)
|
is_ok, detail = check_size(stats, max_lines, max_files, excluded_patterns)
|
||||||
|
|||||||
+1563
-513
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ from devx.ci.check_pr_size import (
|
|||||||
check_size,
|
check_size,
|
||||||
cli,
|
cli,
|
||||||
get_diff_stats,
|
get_diff_stats,
|
||||||
|
has_refactoring_label,
|
||||||
is_excluded,
|
is_excluded,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,3 +131,39 @@ class TestCli:
|
|||||||
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD", "--max-lines", "500"])
|
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD", "--max-lines", "500"])
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
assert "600" in result.output
|
assert "600" in result.output
|
||||||
|
|
||||||
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
||||||
|
@patch("devx.ci.check_pr_size.has_refactoring_label", return_value=True)
|
||||||
|
def test_bypasses_with_refactoring_label(self, mock_label: MagicMock, mock_run: MagicMock) -> None:
|
||||||
|
mock_run.return_value = MagicMock(
|
||||||
|
returncode=0,
|
||||||
|
stdout="300\t300\tsrc/main.py\n",
|
||||||
|
stderr="",
|
||||||
|
)
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
["--base", "origin/master", "--head", "HEAD", "--repo", "owner/repo", "--pr-number", "42"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "bypassed" in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHasRefactoringLabel:
|
||||||
|
@patch("devx.ci.check_pr_size.GiteaClient")
|
||||||
|
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
|
||||||
|
def test_returns_true_when_label_present(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
|
||||||
|
mock_client = mock_client_cls.return_value
|
||||||
|
mock_client.get_pr.return_value = {"labels": [{"name": "refactoring"}, {"name": "bug"}]}
|
||||||
|
assert has_refactoring_label("owner/repo", 42) is True
|
||||||
|
|
||||||
|
@patch("devx.ci.check_pr_size.GiteaClient")
|
||||||
|
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
|
||||||
|
def test_returns_false_when_label_absent(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
|
||||||
|
mock_client = mock_client_cls.return_value
|
||||||
|
mock_client.get_pr.return_value = {"labels": [{"name": "bug"}]}
|
||||||
|
assert has_refactoring_label("owner/repo", 42) is False
|
||||||
|
|
||||||
|
@patch("devx.ci.check_pr_size.get_ci_token", side_effect=Exception("no token"))
|
||||||
|
def test_returns_false_on_error(self, mock_token: MagicMock) -> None:
|
||||||
|
assert has_refactoring_label("owner/repo", 42) is False
|
||||||
|
|||||||
Reference in New Issue
Block a user