diff --git a/pyproject.toml b/pyproject.toml index 6b5b905..9e18204 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,8 +139,12 @@ infrastructure_overrides = [ ] # User-facing overrides — safety override for broad infrastructure patterns -# (empty — add when an infrastructure pattern is too broad) -user_facing_overrides = [] +# devx workflow files (.gitea/**) are reference implementations that +# downstream repos (grm, infra) copy from. Changes to them affect how +# consumer projects run their CI, so they must trigger a release. +user_facing_overrides = [ + ".gitea/**", +] # Tag patterns — additional categories for CI conditional execution # Orthogonal to release impact (user-facing vs infrastructure) diff --git a/src/devx/ci/classify_changes.py b/src/devx/ci/classify_changes.py index d812ee8..1ff98e2 100644 --- a/src/devx/ci/classify_changes.py +++ b/src/devx/ci/classify_changes.py @@ -393,14 +393,15 @@ class ChangeClassifier: tags = self._compute_tags(file_path) # 1. User-facing overrides (highest priority — safety) - if file_path in self._user_overrides: - return FileClassification( - path=file_path, - is_user_facing=True, - reason="User-facing override (safety override)", - matched_rule="user_facing_overrides", - tags=tags, - ) + for pattern in self._user_overrides: + if _matches_glob(file_path, pattern): + return FileClassification( + path=file_path, + is_user_facing=True, + reason=f"User-facing override (matches '{pattern}')", + matched_rule="user_facing_overrides", + tags=tags, + ) # 2. Infrastructure overrides if file_path in self._infra_overrides: diff --git a/tests/unit/test_classify_changes.py b/tests/unit/test_classify_changes.py index c32f4d9..725b2de 100644 --- a/tests/unit/test_classify_changes.py +++ b/tests/unit/test_classify_changes.py @@ -264,6 +264,16 @@ class TestChangeClassifier: assert fc.is_user_facing assert fc.matched_rule == "user_facing_overrides" + def test_user_facing_override_glob_matches_nested(self) -> None: + """User-facing overrides support glob patterns like infrastructure.""" + classifier = self._make_classifier( + infrastructure=[".gitea/**"], + user_facing_overrides=[".gitea/**"], + ) + fc = classifier.classify_file(".gitea/workflows/ci.yml") + assert fc.is_user_facing + assert fc.matched_rule == "user_facing_overrides" + def test_user_facing_override_beats_infrastructure_override(self) -> None: """User-facing overrides beat infrastructure overrides (safety first).""" classifier = self._make_classifier( @@ -489,7 +499,7 @@ class TestMain: @patch("devx.ci.classify_changes.get_changed_files") @patch("devx.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"] + mock_changes.return_value = ["docs/index.md", "README.md"] runner = CliRunner() result = runner.invoke(main, []) assert result.exit_code == 2 @@ -572,7 +582,7 @@ class TestMain: @patch("devx.ci.classify_changes.get_changed_files") @patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0") def test_quiet_workflow_only(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None: - mock_changes.return_value = [".gitea/workflows/ci.yml"] + mock_changes.return_value = ["docs/index.md"] runner = CliRunner() result = runner.invoke(main, ["--quiet"]) assert result.exit_code == 0 @@ -631,7 +641,7 @@ class TestMain: @patch("devx.ci.classify_changes.get_changed_files") @patch("devx.ci.classify_changes.get_latest_tag", return_value="v0.3.0") def test_check_user_facing_false(self, mock_tag: MagicMock, mock_changes: MagicMock) -> None: - mock_changes.return_value = [".gitea/workflows/ci.yml", "tests/test_foo.py"] + mock_changes.return_value = ["docs/index.md", "tests/test_foo.py"] runner = CliRunner() result = runner.invoke(main, ["--check", "user-facing", "--quiet"]) assert result.exit_code == 0