GRM-37: refactor: Split CI scripts, fix release PYTHONPATH, dynamic runner discovery

This commit is contained in:
2026-06-21 20:44:39 +00:00
parent 56eb241b77
commit 5511cba1b0
38 changed files with 908 additions and 422 deletions
+52 -27
View File
@@ -1,4 +1,4 @@
"""Unit tests for scripts/classify_changes.py."""
"""Unit tests for scripts/ci/classify_changes.py."""
from unittest.mock import MagicMock, patch
@@ -6,12 +6,13 @@ import click
import pytest
from click.testing import CliRunner
from scripts.classify_changes import (
from scripts.ci.classify_changes import (
classify_changes,
get_changed_files,
get_latest_tag,
has_user_facing_changes,
is_user_facing,
is_workflow_only,
main,
run_git,
)
@@ -30,8 +31,22 @@ class TestIsUserFacing:
def test_workflow_is_not_user_facing(self) -> None:
assert is_user_facing(".gitea/workflows/ci.yml") is False
def test_scripts_are_not_user_facing(self) -> None:
assert is_user_facing("scripts/release.py") is False
def test_ci_scripts_are_not_user_facing(self) -> None:
assert is_user_facing("scripts/ci/release.py") is False
def test_dev_scripts_are_user_facing(self) -> None:
"""Dev scripts (check_test_speed, configure_repo) are NOT in the
workflow-only allowlist, so they default to user-facing."""
assert is_user_facing("scripts/check_test_speed.py") is True
assert is_user_facing("scripts/configure_repo.py") is True
assert is_user_facing("scripts/install_checkmake.py") is True
def test_shell_scripts_are_not_user_facing(self) -> None:
assert is_user_facing("scripts/setup.sh") is False
assert is_user_facing("scripts/molecule_all.sh") is False
def test_scripts_init_is_not_user_facing(self) -> None:
assert is_user_facing("scripts/__init__.py") is False
def test_docs_are_not_user_facing(self) -> None:
assert is_user_facing("docs/user/getting-started.md") is False
@@ -45,6 +60,16 @@ class TestIsUserFacing:
def test_makefile_is_not_user_facing(self) -> None:
assert is_user_facing("Makefile") is False
def test_unknown_file_defaults_to_user_facing(self) -> None:
"""Safe default: unknown files are user-facing (require release)."""
assert is_user_facing("some/new/file.type") is True
assert is_user_facing("new_root_file.txt") is True
def test_is_workflow_only_inverse(self) -> None:
assert is_workflow_only(".gitea/workflows/ci.yml") is True
assert is_workflow_only("src/gitea_runner_manager/cli.py") is False
assert is_workflow_only("pyproject.toml") is False
class TestClassifyChanges:
def test_all_user_facing(self) -> None:
@@ -78,13 +103,13 @@ class TestClassifyChanges:
class TestGetChangedFiles:
@patch("scripts.classify_changes.run_git")
@patch("scripts.ci.classify_changes.run_git")
def test_returns_file_list(self, mock_run_git: MagicMock) -> None:
mock_run_git.return_value = "file1.py\nfile2.py\nfile3.md"
result = get_changed_files("v0.1.0", "HEAD")
assert result == ["file1.py", "file2.py", "file3.md"]
@patch("scripts.classify_changes.run_git")
@patch("scripts.ci.classify_changes.run_git")
def test_empty_when_no_changes(self, mock_run_git: MagicMock) -> None:
mock_run_git.return_value = ""
result = get_changed_files("v0.1.0", "HEAD")
@@ -92,17 +117,17 @@ class TestGetChangedFiles:
class TestHasUserFacingChanges:
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_changed_files")
def test_true_when_user_facing(self, mock_get: MagicMock) -> None:
mock_get.return_value = ["src/gitea_runner_manager/cli.py", "docs/index.md"]
assert has_user_facing_changes("v0.1.0", "HEAD") is True
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_changed_files")
def test_false_when_workflow_only(self, mock_get: MagicMock) -> None:
mock_get.return_value = [".gitea/workflows/ci.yml", "docs/index.md"]
assert has_user_facing_changes("v0.1.0", "HEAD") is False
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_changed_files")
def test_false_when_no_changes(self, mock_get: MagicMock) -> None:
mock_get.return_value = []
assert has_user_facing_changes("v0.1.0", "HEAD") is False
@@ -121,13 +146,13 @@ class TestGetLatestTag:
class TestRunGit:
@patch("scripts.classify_changes.subprocess.run")
@patch("scripts.ci.classify_changes.subprocess.run")
def test_success(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stdout="file1.py\n", stderr="")
result = run_git(["git", "diff", "--name-only", "v0.1.0", "HEAD"])
assert result == "file1.py"
@patch("scripts.classify_changes.subprocess.run")
@patch("scripts.ci.classify_changes.subprocess.run")
def test_failure_raises(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="git error")
with pytest.raises(click.ClickException):
@@ -135,23 +160,23 @@ class TestRunGit:
class TestMain:
@patch("scripts.classify_changes.get_latest_tag", return_value="")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="")
def test_no_tags_outputs_true(self, mock_tag: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--quiet"])
assert result.exit_code == 0
assert "true" in result.output
@patch("scripts.classify_changes.get_changed_files", return_value=[])
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files", return_value=[])
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_no_changes_outputs_false(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--quiet"])
assert result.exit_code == 0
assert "false" in result.output
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_workflow_only_exits_2(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
mock_changes.return_value = [".gitea/workflows/ci.yml", "docs/index.md"]
runner = CliRunner()
@@ -159,8 +184,8 @@ class TestMain:
assert result.exit_code == 2
assert "no release needed" in result.output
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_user_facing_exits_0(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
mock_changes.return_value = ["src/gitea_runner_manager/cli.py", "docs/index.md"]
runner = CliRunner()
@@ -168,7 +193,7 @@ class TestMain:
assert result.exit_code == 0
assert "release needed" in result.output
@patch("scripts.classify_changes.get_latest_tag", return_value="")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="")
def test_no_tags_non_quiet(self, mock_tag: MagicMock) -> None:
"""Non-quiet mode with no tags prints user-facing message."""
runner = CliRunner()
@@ -176,8 +201,8 @@ class TestMain:
assert result.exit_code == 0
assert "No tags found" in result.output
@patch("scripts.classify_changes.get_changed_files", return_value=[])
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files", return_value=[])
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_no_changes_non_quiet(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
"""Non-quiet mode with no changes prints message."""
runner = CliRunner()
@@ -185,8 +210,8 @@ class TestMain:
assert result.exit_code == 0
assert "No changes" in result.output
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_quiet_user_facing(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
"""Quiet mode with user-facing changes outputs true."""
mock_changes.return_value = ["src/gitea_runner_manager/cli.py"]
@@ -195,8 +220,8 @@ class TestMain:
assert result.exit_code == 0
assert "true" in result.output
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_quiet_workflow_only(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
"""Quiet mode with workflow-only changes outputs false."""
mock_changes.return_value = [".gitea/workflows/ci.yml"]
@@ -205,8 +230,8 @@ class TestMain:
assert result.exit_code == 0
assert "false" in result.output
@patch("scripts.classify_changes.get_changed_files")
@patch("scripts.classify_changes.get_latest_tag", return_value="v0.3.0")
@patch("scripts.ci.classify_changes.get_changed_files")
@patch("scripts.ci.classify_changes.get_latest_tag", return_value="v0.3.0")
def test_with_explicit_base(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None:
"""Explicit --base overrides latest tag."""
mock_changes.return_value = ["src/gitea_runner_manager/cli.py"]