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 ed1083acf41f9d..c39dc715774986 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -101,6 +101,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. @@ -130,6 +137,8 @@ class VstsIntegration(RepositoryIntegration, VstsIssuesSpec): outbound_assignee_key = "sync_forward_assignment" inbound_assignee_key = "sync_reverse_assignment" + codeowners_locations = ["CODEOWNERS", ".sentry/CODEOWNERS"] + @property def integration_name(self) -> str: return "vsts" @@ -405,6 +414,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 7b00efe044d6fc..269c60ca18a087 100644 --- a/src/sentry/models/commitfilechange.py +++ b/src/sentry/models/commitfilechange.py @@ -54,6 +54,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(): @@ -61,6 +62,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()