diff --git a/src/sentry/issues/ownership/grammar.py b/src/sentry/issues/ownership/grammar.py index f7baedb2238a..7249c9a781a3 100644 --- a/src/sentry/issues/ownership/grammar.py +++ b/src/sentry/issues/ownership/grammar.py @@ -51,6 +51,7 @@ class OwnershipRule(TypedDict): PATH = "path" MODULE = "module" CODEOWNERS = "codeowners" +EMPTY_CODEOWNERS_MATCHER = re.compile(rf"^\s*{CODEOWNERS}:\s*(?:#.*)?$") # Grammar is defined in EBNF syntax. ownership_grammar = Grammar( @@ -327,6 +328,14 @@ def parse_rules(data: str) -> Any: return OwnershipVisitor().visit(tree) +def remove_empty_codeowners_matchers(data: str) -> str: + return "".join( + line + for line in data.splitlines(keepends=True) + if not EMPTY_CODEOWNERS_MATCHER.match(line) + ) + + def dump_schema(rules: Sequence[Rule]) -> OwnershipSchema: """Convert a Rule tree into a JSON schema""" return {"$version": VERSION, "rules": [r.dump() for r in rules]} @@ -461,6 +470,9 @@ def convert_codeowners_syntax( else: formatted_path = path + if not formatted_path: + formatted_path = "/" + if not code_owners: # Exclusion rule: path with no owners means "no ownership" for this path result += f"codeowners:{formatted_path}\n" @@ -620,6 +632,8 @@ def create_schema_from_issue_owners( if issue_owners is None: return None + issue_owners = remove_empty_codeowners_matchers(issue_owners) + try: rules = parse_rules(issue_owners) except ParseError as e: diff --git a/tests/sentry/issues/ownership/test_grammar.py b/tests/sentry/issues/ownership/test_grammar.py index 90d508433c11..552996ea2422 100644 --- a/tests/sentry/issues/ownership/test_grammar.py +++ b/tests/sentry/issues/ownership/test_grammar.py @@ -9,6 +9,7 @@ Rule, convert_codeowners_syntax, convert_schema_to_rules_text, + create_schema_from_issue_owners, dump_schema, get_invalid_owner_details, load_schema, @@ -1422,6 +1423,43 @@ def test_convert_codeowners_syntax_exclusion_with_stack_root() -> None: assert "codeowners:webpack://static/apps/github\n" in result +def test_convert_codeowners_syntax_preserves_mapped_root_path() -> None: + code_mapping = type("", (), {})() + code_mapping.stack_root = "" + code_mapping.source_root = "/apps" + + result = convert_codeowners_syntax( + "/apps @octocat\n", + {"@octocat": "octocat@sentry.io"}, + code_mapping, + ) + assert result == "codeowners:/ octocat@sentry.io\n" + + +def test_convert_codeowners_syntax_preserves_mapped_root_exclusion() -> None: + code_mapping = type("", (), {})() + code_mapping.stack_root = "" + code_mapping.source_root = "/apps" + + result = convert_codeowners_syntax( + "/apps\n", + {}, + code_mapping, + ) + assert result == "codeowners:/\n" + + +def test_create_schema_from_issue_owners_skips_empty_codeowners_matcher() -> None: + assert ( + create_schema_from_issue_owners( + project_id=1, + issue_owners="codeowners: #discover\n", + remove_deleted_owners=True, + ) + == {"$version": 1, "rules": []} + ) + + def test_parse_code_owners_exclusion_rule() -> None: codeowners = "/apps/ @getsentry/frontend\n/apps/github\n" teams, usernames, emails = parse_code_owners(codeowners) diff --git a/tests/sentry/tasks/test_code_owners.py b/tests/sentry/tasks/test_code_owners.py index bb37f518e0c1..2d9f618bd3c4 100644 --- a/tests/sentry/tasks/test_code_owners.py +++ b/tests/sentry/tasks/test_code_owners.py @@ -87,6 +87,32 @@ def test_simple(self) -> None: assert code_owners.schema == {"$version": 1, "rules": []} assert code_owners.date_synced is None + def test_update_schema_with_codeowners_root_mapping(self) -> None: + self.code_mapping.source_root = "/docs" + self.code_mapping.stack_root = "" + self.code_mapping.save() + self.code_owners.raw = "/docs @getsentry/ecosystem\n" + self.code_owners.save() + + with self.tasks() and self.feature({"organizations:integrations-codeowners": True}): + self.create_external_team(integration=self.integration) + update_code_owners_schema( + organization=self.organization.id, integration=self.integration.id + ) + + code_owners = ProjectCodeOwners.objects.get(id=self.code_owners.id) + assert code_owners.schema == { + "$version": 1, + "rules": [ + { + "matcher": {"type": "codeowners", "pattern": "/"}, + "owners": [ + {"type": "team", "identifier": "tiger-team", "id": self.team.id}, + ], + } + ], + } + @freeze_time("2023-01-01 00:00:00") @patch( "sentry.integrations.github.integration.GitHubIntegration.get_codeowner_file",