From 64d53a3818d27dd20d47add837e10ca8c8900d76 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Wed, 12 Mar 2025 23:11:03 +0000 Subject: [PATCH] feat(azure-devops): CODEOWNERS support --- src/sentry/integrations/vsts/client.py | 22 +++++++++++--- src/sentry/integrations/vsts/integration.py | 10 +++++++ src/sentry/models/commitfilechange.py | 2 ++ tests/sentry/integrations/vsts/test_client.py | 29 +++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/sentry/integrations/vsts/client.py b/src/sentry/integrations/vsts/client.py index 46a9cd46156aef..50ba3c1d123364 100644 --- a/src/sentry/integrations/vsts/client.py +++ b/src/sentry/integrations/vsts/client.py @@ -10,7 +10,6 @@ from sentry.constants import ObjectStatus from sentry.exceptions import InvalidIdentity -from sentry.integrations.base import IntegrationFeatureNotImplementedError from sentry.integrations.client import ApiClient from sentry.integrations.services.integration.service import integration_service from sentry.integrations.source_code_management.repository import RepositoryClient @@ -172,12 +171,12 @@ def identity(self): def request(self, method: str, *args: Any, **kwargs: Any) -> Any: api_preview = kwargs.pop("api_preview", False) - new_headers = prepare_headers( + base_headers = prepare_headers( api_version=self.api_version, method=method, api_version_preview=self.api_version_preview if api_preview else "", ) - kwargs.setdefault("headers", {}).update(new_headers) + kwargs["headers"] = {**base_headers, **(kwargs.get("headers", {}))} return self._request(method, *args, **kwargs) @@ -450,4 +449,19 @@ def check_file(self, repo: Repository, path: str, version: str | None) -> object def get_file( self, repo: Repository, path: str, ref: str | None, codeowners: bool = False ) -> str: - raise IntegrationFeatureNotImplementedError + response = self.get_cached( + path=VstsApiPath.items.format( + instance=repo.config["instance"], + project=quote(repo.config["project"]), + repo_id=quote(repo.config["name"]), + ), + params={ + "path": path, + "api-version": "7.0", + "versionDescriptor.version": ref, + "download": "true", + }, + headers={"Accept": "*/*"}, + raw_response=True, + ) + return response.text diff --git a/src/sentry/integrations/vsts/integration.py b/src/sentry/integrations/vsts/integration.py index d838ca3fac596f..d52f5d0e19e7c4 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -100,6 +100,13 @@ """, IntegrationFeatures.STACKTRACE_LINK, ), + FeatureDescription( + """ + Import your Azure DevOps codeowners file into Sentry and use it alongside your + ownership rules to assign Sentry issues. + """, + IntegrationFeatures.CODEOWNERS, + ), FeatureDescription( """ Automatically create Azure DevOps work items based on Issue Alert conditions. @@ -129,6 +136,8 @@ class VstsIntegration(RepositoryIntegration, VstsIssuesSpec): outbound_assignee_key = "sync_forward_assignment" inbound_assignee_key = "sync_reverse_assignment" + codeowners_locations = ["CODEOWNERS", ".sentry/CODEOWNERS"] + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.default_identity: RpcIdentity | None = None @@ -406,6 +415,7 @@ class VstsIntegrationProvider(IntegrationProvider): IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.ISSUE_SYNC, IntegrationFeatures.STACKTRACE_LINK, + IntegrationFeatures.CODEOWNERS, IntegrationFeatures.TICKET_RULES, ] ) diff --git a/src/sentry/models/commitfilechange.py b/src/sentry/models/commitfilechange.py index 902424b93dc2ac..afe3570a8e4e1e 100644 --- a/src/sentry/models/commitfilechange.py +++ b/src/sentry/models/commitfilechange.py @@ -51,6 +51,7 @@ def process_resource_change(instance, **kwargs): from sentry.integrations.bitbucket.integration import BitbucketIntegration from sentry.integrations.github.integration import GitHubIntegration from sentry.integrations.gitlab.integration import GitlabIntegration + from sentry.integrations.vsts.integration import VstsIntegration from sentry.tasks.codeowners import code_owners_auto_sync def _spawn_task(): @@ -58,6 +59,7 @@ def _spawn_task(): set(GitHubIntegration.codeowners_locations) | set(GitlabIntegration.codeowners_locations) | set(BitbucketIntegration.codeowners_locations) + | set(VstsIntegration.codeowners_locations) ) # CODEOWNERS file added or modified, trigger auto-sync diff --git a/tests/sentry/integrations/vsts/test_client.py b/tests/sentry/integrations/vsts/test_client.py index 6915aa34bc240c..98b35f89503c15 100644 --- a/tests/sentry/integrations/vsts/test_client.py +++ b/tests/sentry/integrations/vsts/test_client.py @@ -282,6 +282,35 @@ def test_check_no_file(self): with pytest.raises(ApiError): client.check_file(repo, path, version) + @responses.activate + def test_get_file(self): + self.assert_installation() + integration, installation = self._get_integration_and_install() + with assume_test_silo_mode(SiloMode.REGION): + repo = Repository.objects.create( + provider="visualstudio", + name="example", + organization_id=self.organization.id, + config={ + "instance": self.vsts_base_url, + "project": "project-name", + "name": "example", + }, + integration_id=integration.id, + external_id="albertos-apples", + ) + + client = installation.get_client() + + path = "README.md" + version = "master" + url = f"https://myvstsaccount.visualstudio.com/project-name/_apis/git/repositories/{repo.name}/items?path={path}&api-version=7.0&versionDescriptor.version={version}&download=true" + + responses.add(method=responses.GET, url=url, body="Hello, world!") + + resp = client.get_file(repo, path, version) + assert resp == "Hello, world!" + @responses.activate def test_get_stacktrace_link(self): self.assert_installation()