feat: add refactoring label bypass for check_pr_size
CI / validate (pull_request) Successful in 1m28s
CI / auto-merge (pull_request) Failing after 19s

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:
emil
2026-08-24 22:06:51 +02:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 0de18ebb21
commit 53c4823e04
4 changed files with 1636 additions and 513 deletions
+3
View File
@@ -121,11 +121,14 @@ jobs:
if: github.event_name == 'pull_request'
env:
PYTHONPATH: ${{ env.PYTHONPATH }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_pr_size \
--base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
--repo "${{ github.repository }}" \
--pr-number "${{ github.event.number }}" \
--github-output
# --- release-dry-run step (conditional) ---
- name: Release dry-run validation
+33
View File
@@ -5,6 +5,9 @@
Enforces max lines changed and max files changed to keep PRs small
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:
python -m devx.ci.check_pr_size --base origin/master --head HEAD
@@ -19,8 +22,11 @@ import subprocess # nosec B404
import click
from dotenv import load_dotenv
from devx.api_clients import GiteaClient
from devx.ci._shared import write_github_output
from devx.config import GITEA_API_URL
from devx.i18n import _
from devx.tokens import get_ci_token
load_dotenv()
@@ -40,6 +46,20 @@ DEFAULT_EXCLUDED_PATTERNS = [
DEFAULT_MAX_LINES = 500
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]]:
@@ -147,6 +167,8 @@ def check_size(
multiple=True,
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(
base: str,
head: str,
@@ -154,8 +176,19 @@ def cli(
max_files: int,
github_output: bool,
excluded: tuple[str, ...],
repo: str | None,
pr_number: int | None,
) -> None:
"""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)
stats = get_diff_stats(base, head)
is_ok, detail = check_size(stats, max_lines, max_files, excluded_patterns)
+1563 -513
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -8,6 +8,7 @@ from devx.ci.check_pr_size import (
check_size,
cli,
get_diff_stats,
has_refactoring_label,
is_excluded,
)
@@ -130,3 +131,39 @@ class TestCli:
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD", "--max-lines", "500"])
assert result.exit_code != 0
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