From 8fb82e777e602659c8b2b33ac13aed705147faa2 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 1 Apr 2026 15:04:47 -0700 Subject: [PATCH 1/3] feat(repos): Add audit logs when auto syncing repositories Add in audit logs whenever we automatically add, update or disable a repository via webhooks and periodic syncing --- src/sentry/audit_log/events.py | 35 +++++++++++++++++++ src/sentry/audit_log/register.py | 3 ++ .../integrations/github/tasks/sync_repos.py | 33 ++++++++++++++++- .../tasks/sync_repos_on_install_change.py | 33 ++++++++++++++++- .../source_code_management/repo_audit.py | 23 ++++++++++++ .../providers/integration_repository.py | 6 +++- .../github/tasks/test_sync_repos.py | 27 +++++++++++++- .../test_sync_repos_on_install_change.py | 17 ++++++++- 8 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/sentry/integrations/source_code_management/repo_audit.py diff --git a/src/sentry/audit_log/events.py b/src/sentry/audit_log/events.py index cbfd66a14e44da..1f5f1d80b62ea8 100644 --- a/src/sentry/audit_log/events.py +++ b/src/sentry/audit_log/events.py @@ -384,3 +384,38 @@ def render(self, audit_log_entry: AuditLogEntry) -> str: return "updated repository settings for {repository_count} repositories".format( repository_count=data.get("repository_count", 0), ) + + +def _render_repo_event(action: str, audit_log_entry: AuditLogEntry) -> str: + data = audit_log_entry.data + actor = audit_log_entry.actor_label or "unknown" + repo_name = data.get("repo_name", "unknown") + source = data.get("source", "") + msg = f"{actor} {action} repository {repo_name}" + if source: + msg += f" (via {source})" + return msg + + +class RepoAddedAuditLogEvent(AuditLogEvent): + def __init__(self) -> None: + super().__init__(event_id=1161, name="REPO_ADDED", api_name="repo.added") + + def render(self, audit_log_entry: AuditLogEntry) -> str: + return _render_repo_event("added", audit_log_entry) + + +class RepoDisabledAuditLogEvent(AuditLogEvent): + def __init__(self) -> None: + super().__init__(event_id=1162, name="REPO_DISABLED", api_name="repo.disabled") + + def render(self, audit_log_entry: AuditLogEntry) -> str: + return _render_repo_event("disabled", audit_log_entry) + + +class RepoEnabledAuditLogEvent(AuditLogEvent): + def __init__(self) -> None: + super().__init__(event_id=1163, name="REPO_ENABLED", api_name="repo.enabled") + + def render(self, audit_log_entry: AuditLogEntry) -> str: + return _render_repo_event("enabled", audit_log_entry) diff --git a/src/sentry/audit_log/register.py b/src/sentry/audit_log/register.py index 0cbb57d9408502..b47bf850283fe0 100644 --- a/src/sentry/audit_log/register.py +++ b/src/sentry/audit_log/register.py @@ -695,3 +695,6 @@ template="updated autofix automation settings for {project_count} projects", ) ) +default_manager.add(events.RepoAddedAuditLogEvent()) +default_manager.add(events.RepoDisabledAuditLogEvent()) +default_manager.add(events.RepoEnabledAuditLogEvent()) diff --git a/src/sentry/integrations/github/tasks/sync_repos.py b/src/sentry/integrations/github/tasks/sync_repos.py index ebfd8a6960c1b2..00bd3ebacc02d6 100644 --- a/src/sentry/integrations/github/tasks/sync_repos.py +++ b/src/sentry/integrations/github/tasks/sync_repos.py @@ -21,6 +21,7 @@ SCMIntegrationInteractionEvent, SCMIntegrationInteractionType, ) +from sentry.integrations.source_code_management.repo_audit import log_repo_change from sentry.organizations.services.organization import organization_service from sentry.plugins.providers.integration_repository import ( RepoExistsError, @@ -171,6 +172,8 @@ def sync_repos_for_org(organization_integration_id: int) -> None: if dry_run: return + repo_by_external_id = {r.external_id: r for r in active_repos + disabled_repos} + if new_ids: integration_repo_provider = get_integration_repository_provider(integration) repo_configs = [ @@ -179,13 +182,23 @@ def sync_repos_for_org(organization_integration_id: int) -> None: if str(repo["id"]) in new_ids ] if repo_configs: + created_repos = [] try: - integration_repo_provider.create_repositories( + created_repos = integration_repo_provider.create_repositories( configs=repo_configs, organization=rpc_org ) except RepoExistsError: pass + for repo in created_repos: + log_repo_change( + event_name="REPO_ADDED", + organization_id=organization_id, + repo=repo, + source="repository sync", + provider=integration.provider, + ) + if removed_ids: repository_service.disable_repositories_by_external_ids( organization_id=organization_id, @@ -194,6 +207,17 @@ def sync_repos_for_org(organization_integration_id: int) -> None: external_ids=list(removed_ids), ) + for eid in removed_ids: + repo = repo_by_external_id.get(eid) + if repo: + log_repo_change( + event_name="REPO_DISABLED", + organization_id=organization_id, + repo=repo, + source="repository sync", + provider=integration.provider, + ) + if restored_ids: for repo in disabled_repos: if repo.external_id in restored_ids: @@ -201,6 +225,13 @@ def sync_repos_for_org(organization_integration_id: int) -> None: repository_service.update_repository( organization_id=organization_id, update=repo ) + log_repo_change( + event_name="REPO_ENABLED", + organization_id=organization_id, + repo=repo, + source="repository sync", + provider=integration.provider, + ) @instrumented_task( diff --git a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py index c3ab3b70155163..f73015c9200b17 100644 --- a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py +++ b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py @@ -13,6 +13,7 @@ SCMIntegrationInteractionEvent, SCMIntegrationInteractionType, ) +from sentry.integrations.source_code_management.repo_audit import log_repo_change from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import RpcOrganization from sentry.plugins.providers.integration_repository import ( @@ -119,18 +120,48 @@ def _sync_repos_for_org( continue if repo_configs: + created_repos = [] try: - integration_repo_provider.create_repositories( + created_repos = integration_repo_provider.create_repositories( configs=repo_configs, organization=rpc_org ) except RepoExistsError: pass + for created_repo in created_repos: + log_repo_change( + event_name="REPO_ADDED", + organization_id=rpc_org.id, + repo=created_repo, + source="GitHub webhook", + provider=integration.provider, + ) + if repos_removed: + # Look up repos before disabling to get their IDs and names external_ids = [str(repo["id"]) for repo in repos_removed] + existing_repos = repository_service.get_repositories( + organization_id=rpc_org.id, + integration_id=integration.id, + providers=[provider], + ) + repo_by_eid = {r.external_id: r for r in existing_repos if r.external_id} + repository_service.disable_repositories_by_external_ids( organization_id=rpc_org.id, integration_id=integration.id, provider=provider, external_ids=external_ids, ) + + for repo in repos_removed: + eid = str(repo["id"]) + sentry_repo = repo_by_eid.get(eid) + if sentry_repo: + log_repo_change( + event_name="REPO_DISABLED", + organization_id=rpc_org.id, + repo=sentry_repo, + source="GitHub webhook", + provider=integration.provider, + ) diff --git a/src/sentry/integrations/source_code_management/repo_audit.py b/src/sentry/integrations/source_code_management/repo_audit.py new file mode 100644 index 00000000000000..618fbeacca7db6 --- /dev/null +++ b/src/sentry/integrations/source_code_management/repo_audit.py @@ -0,0 +1,23 @@ +""" +Audit log helpers for repository sync operations. +""" + +from sentry import audit_log +from sentry.integrations.services.repository.model import RpcRepository +from sentry.utils.audit import create_system_audit_entry + + +def log_repo_change( + *, event_name: str, organization_id: int, repo: RpcRepository, source: str, provider: str +) -> None: + create_system_audit_entry( + organization_id=organization_id, + target_object=repo.id, + event=audit_log.get_event_id(event_name), + data={ + "repo_name": repo.name, + "external_id": repo.external_id, + "source": source, + "provider": provider, + }, + ) diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index e238eccd3124cc..178c8481964347 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -240,13 +240,14 @@ def create_repositories( self, configs: list[RepositoryInputConfig], organization: RpcOrganization, - ): + ) -> list[RpcRepository]: external_id_to_repo_config: dict[str, RepositoryConfig] = {} for config in configs: result = self.build_repository_config(organization=organization, data=config) external_id_to_repo_config[result["external_id"]] = result repos_to_update: list[RpcRepository] = [] + created_repos: list[RpcRepository] = [] hidden_repos = repository_service.get_repositories( organization_id=organization.id, @@ -272,6 +273,7 @@ def create_repositories( organization_id=organization.id, create=create_repository ) if new_repository is not None: + created_repos.append(new_repository) continue missing_repos.append(repo_config) @@ -302,6 +304,8 @@ def create_repositories( if missing_repos: raise RepoExistsError(repos=missing_repos) + return created_repos + repos_to_update + def dispatch(self, request: Request, organization, **kwargs): try: config = self.get_repository_data(organization, request.data) diff --git a/tests/sentry/integrations/github/tasks/test_sync_repos.py b/tests/sentry/integrations/github/tasks/test_sync_repos.py index c57e4da1189afa..f6f224b0c737be 100644 --- a/tests/sentry/integrations/github/tasks/test_sync_repos.py +++ b/tests/sentry/integrations/github/tasks/test_sync_repos.py @@ -4,14 +4,16 @@ import responses from taskbroker_client.retry import RetryTaskError +from sentry import audit_log from sentry.constants import ObjectStatus from sentry.integrations.github.integration import GitHubIntegrationProvider from sentry.integrations.github.tasks.sync_repos import sync_repos_for_org from sentry.integrations.models.organization_integration import OrganizationIntegration +from sentry.models.auditlogentry import AuditLogEntry from sentry.models.repository import Repository from sentry.silo.base import SiloMode from sentry.testutils.cases import IntegrationTestCase -from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test @control_silo_test @@ -60,6 +62,13 @@ def test_creates_new_repos(self, _: MagicMock) -> None: assert repos[0].provider == "integrations:github" assert repos[1].name == "getsentry/snuba" + with assume_test_silo_mode_of(AuditLogEntry): + entries = AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_ADDED"), + ) + assert entries.count() == 2 + @responses.activate def test_disables_removed_repos(self, _: MagicMock) -> None: with assume_test_silo_mode(SiloMode.CELL): @@ -89,6 +98,16 @@ def test_disables_removed_repos(self, _: MagicMock) -> None: organization_id=self.organization.id, external_id="1" ).exists() + with assume_test_silo_mode_of(AuditLogEntry): + assert AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_DISABLED"), + ).exists() + assert AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_ADDED"), + ).exists() + @responses.activate def test_re_enables_restored_repos(self, _: MagicMock) -> None: with assume_test_silo_mode(SiloMode.CELL): @@ -113,6 +132,12 @@ def test_re_enables_restored_repos(self, _: MagicMock) -> None: repo.refresh_from_db() assert repo.status == ObjectStatus.ACTIVE + with assume_test_silo_mode_of(AuditLogEntry): + assert AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_ENABLED"), + ).exists() + @responses.activate def test_no_changes_needed(self, _: MagicMock) -> None: with assume_test_silo_mode(SiloMode.CELL): diff --git a/tests/sentry/integrations/github/tasks/test_sync_repos_on_install_change.py b/tests/sentry/integrations/github/tasks/test_sync_repos_on_install_change.py index 9f63922c72d299..b70b55f55b6f99 100644 --- a/tests/sentry/integrations/github/tasks/test_sync_repos_on_install_change.py +++ b/tests/sentry/integrations/github/tasks/test_sync_repos_on_install_change.py @@ -1,14 +1,16 @@ from unittest.mock import MagicMock, patch +from sentry import audit_log from sentry.constants import ObjectStatus from sentry.integrations.github.integration import GitHubIntegrationProvider from sentry.integrations.github.tasks.sync_repos_on_install_change import ( sync_repos_on_install_change, ) +from sentry.models.auditlogentry import AuditLogEntry from sentry.models.repository import Repository from sentry.silo.base import SiloMode from sentry.testutils.cases import IntegrationTestCase -from sentry.testutils.silo import assume_test_silo_mode, control_silo_test +from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test FEATURE_FLAG = "organizations:github-repo-auto-sync" @@ -50,6 +52,13 @@ def test_repos_added(self, _: MagicMock) -> None: assert repos[0].integration_id == self.integration.id assert repos[1].name == "getsentry/snuba" + with assume_test_silo_mode_of(AuditLogEntry): + entries = AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_ADDED"), + ) + assert entries.count() == 2 + def test_repos_removed(self, _: MagicMock) -> None: with assume_test_silo_mode(SiloMode.CELL): repo = Repository.objects.create( @@ -74,6 +83,12 @@ def test_repos_removed(self, _: MagicMock) -> None: repo.refresh_from_db() assert repo.status == ObjectStatus.DISABLED + with assume_test_silo_mode_of(AuditLogEntry): + assert AuditLogEntry.objects.filter( + organization_id=self.organization.id, + event=audit_log.get_event_id("REPO_DISABLED"), + ).exists() + def test_mixed_add_and_remove(self, _: MagicMock) -> None: with assume_test_silo_mode(SiloMode.CELL): old_repo = Repository.objects.create( From f4665172b7b349fe34d621d0b99386e392f9f244 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 6 Apr 2026 15:14:32 -0700 Subject: [PATCH 2/3] address pr comments --- .../integrations/github/tasks/sync_repos.py | 29 ++++++++++++++----- .../tasks/sync_repos_on_install_change.py | 21 ++++++++++++-- .../providers/integration_repository.py | 9 ++++-- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/sentry/integrations/github/tasks/sync_repos.py b/src/sentry/integrations/github/tasks/sync_repos.py index 00bd3ebacc02d6..80b737e4a5e4cc 100644 --- a/src/sentry/integrations/github/tasks/sync_repos.py +++ b/src/sentry/integrations/github/tasks/sync_repos.py @@ -16,6 +16,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.integrations.services.integration import integration_service +from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.services.repository.service import repository_service from sentry.integrations.source_code_management.metrics import ( SCMIntegrationInteractionEvent, @@ -182,10 +183,13 @@ def sync_repos_for_org(organization_integration_id: int) -> None: if str(repo["id"]) in new_ids ] if repo_configs: - created_repos = [] + created_repos: list[RpcRepository] = [] + reactivated_repos: list[RpcRepository] = [] try: - created_repos = integration_repo_provider.create_repositories( - configs=repo_configs, organization=rpc_org + created_repos, reactivated_repos = ( + integration_repo_provider.create_repositories( + configs=repo_configs, organization=rpc_org + ) ) except RepoExistsError: pass @@ -199,6 +203,15 @@ def sync_repos_for_org(organization_integration_id: int) -> None: provider=integration.provider, ) + for repo in reactivated_repos: + log_repo_change( + event_name="REPO_ENABLED", + organization_id=organization_id, + repo=repo, + source="repository sync", + provider=integration.provider, + ) + if removed_ids: repository_service.disable_repositories_by_external_ids( organization_id=organization_id, @@ -208,13 +221,13 @@ def sync_repos_for_org(organization_integration_id: int) -> None: ) for eid in removed_ids: - repo = repo_by_external_id.get(eid) - if repo: + removed_repo = repo_by_external_id.get(eid) + if removed_repo: log_repo_change( event_name="REPO_DISABLED", organization_id=organization_id, - repo=repo, - source="repository sync", + repo=removed_repo, + source="automatic SCM syncing", provider=integration.provider, ) @@ -229,7 +242,7 @@ def sync_repos_for_org(organization_integration_id: int) -> None: event_name="REPO_ENABLED", organization_id=organization_id, repo=repo, - source="repository sync", + source="automatic SCM syncing", provider=integration.provider, ) diff --git a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py index f73015c9200b17..8fe33d092f470f 100644 --- a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py +++ b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py @@ -8,6 +8,7 @@ from sentry.integrations.github.webhook_types import GitHubInstallationRepo from sentry.integrations.services.integration import integration_service from sentry.integrations.services.integration.model import RpcIntegration +from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.services.repository.service import repository_service from sentry.integrations.source_code_management.metrics import ( SCMIntegrationInteractionEvent, @@ -120,9 +121,10 @@ def _sync_repos_for_org( continue if repo_configs: - created_repos = [] + created_repos: list[RpcRepository] = [] + reactivated_repos: list[RpcRepository] = [] try: - created_repos = integration_repo_provider.create_repositories( + created_repos, reactivated_repos = integration_repo_provider.create_repositories( configs=repo_configs, organization=rpc_org ) except RepoExistsError: @@ -137,6 +139,15 @@ def _sync_repos_for_org( provider=integration.provider, ) + for reactivated_repo in reactivated_repos: + log_repo_change( + event_name="REPO_ENABLED", + organization_id=rpc_org.id, + repo=reactivated_repo, + source="GitHub webhook", + provider=integration.provider, + ) + if repos_removed: # Look up repos before disabling to get their IDs and names external_ids = [str(repo["id"]) for repo in repos_removed] @@ -145,7 +156,11 @@ def _sync_repos_for_org( integration_id=integration.id, providers=[provider], ) - repo_by_eid = {r.external_id: r for r in existing_repos if r.external_id} + repo_by_eid = { + r.external_id: r + for r in existing_repos + if r.external_id and r.status == ObjectStatus.ACTIVE + } repository_service.disable_repositories_by_external_ids( organization_id=rpc_org.id, diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index 178c8481964347..20170a4f567c05 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -240,7 +240,12 @@ def create_repositories( self, configs: list[RepositoryInputConfig], organization: RpcOrganization, - ) -> list[RpcRepository]: + ) -> tuple[list[RpcRepository], list[RpcRepository]]: + """ + Create or update repositories from configs. + Returns (created, reactivated) — newly created repos and repos that + were reactivated or updated from a hidden/unlinked state. + """ external_id_to_repo_config: dict[str, RepositoryConfig] = {} for config in configs: result = self.build_repository_config(organization=organization, data=config) @@ -304,7 +309,7 @@ def create_repositories( if missing_repos: raise RepoExistsError(repos=missing_repos) - return created_repos + repos_to_update + return created_repos, repos_to_update def dispatch(self, request: Request, organization, **kwargs): try: From 784ec6ecf8013b3406e4b00d0752a66c9907cabe Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Tue, 7 Apr 2026 09:48:52 -0700 Subject: [PATCH 3/3] don't miss audit logs --- .../github/tasks/link_all_repos.py | 10 +++++----- .../integrations/github/tasks/sync_repos.py | 19 ++++--------------- .../tasks/sync_repos_on_install_change.py | 13 ++++--------- .../providers/integration_repository.py | 13 ++++++------- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/sentry/integrations/github/tasks/link_all_repos.py b/src/sentry/integrations/github/tasks/link_all_repos.py index 046c0fe466236f..2eae7324bac0f4 100644 --- a/src/sentry/integrations/github/tasks/link_all_repos.py +++ b/src/sentry/integrations/github/tasks/link_all_repos.py @@ -13,7 +13,6 @@ ) from sentry.organizations.services.organization import organization_service from sentry.plugins.providers.integration_repository import ( - RepoExistsError, RepositoryInputConfig, get_integration_repository_provider, ) @@ -40,7 +39,7 @@ def get_repo_config(repo: Mapping[str, Any], integration_id: int) -> RepositoryI processing_deadline_duration=60, silo_mode=SiloMode.CONTROL, ) -@retry(exclude=(RepoExistsError, KeyError)) +@retry(exclude=(KeyError,)) def link_all_repos( integration_key: str, integration_id: int, @@ -88,14 +87,15 @@ def link_all_repos( missing_repos.append(repo) continue - try: + _created_repos, _reactivated_repos, existing_repos = ( integration_repo_provider.create_repositories( configs=repo_configs, organization=rpc_org ) - except RepoExistsError as e: + ) + if existing_repos: lifecycle.record_halt( str(LinkAllReposHaltReason.REPOSITORY_NOT_CREATED), - {"missing_repos": e.repos, "integration_id": integration_id}, + {"missing_repos": existing_repos, "integration_id": integration_id}, ) if missing_repos: diff --git a/src/sentry/integrations/github/tasks/sync_repos.py b/src/sentry/integrations/github/tasks/sync_repos.py index 80b737e4a5e4cc..df2e37c1882f33 100644 --- a/src/sentry/integrations/github/tasks/sync_repos.py +++ b/src/sentry/integrations/github/tasks/sync_repos.py @@ -16,7 +16,6 @@ from sentry.constants import ObjectStatus from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.integrations.services.integration import integration_service -from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.services.repository.service import repository_service from sentry.integrations.source_code_management.metrics import ( SCMIntegrationInteractionEvent, @@ -24,10 +23,7 @@ ) from sentry.integrations.source_code_management.repo_audit import log_repo_change from sentry.organizations.services.organization import organization_service -from sentry.plugins.providers.integration_repository import ( - RepoExistsError, - get_integration_repository_provider, -) +from sentry.plugins.providers.integration_repository import get_integration_repository_provider from sentry.shared_integrations.exceptions import ApiError from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry @@ -183,16 +179,9 @@ def sync_repos_for_org(organization_integration_id: int) -> None: if str(repo["id"]) in new_ids ] if repo_configs: - created_repos: list[RpcRepository] = [] - reactivated_repos: list[RpcRepository] = [] - try: - created_repos, reactivated_repos = ( - integration_repo_provider.create_repositories( - configs=repo_configs, organization=rpc_org - ) - ) - except RepoExistsError: - pass + created_repos, reactivated_repos, _ = integration_repo_provider.create_repositories( + configs=repo_configs, organization=rpc_org + ) for repo in created_repos: log_repo_change( diff --git a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py index 8fe33d092f470f..3b8b65e97d75e0 100644 --- a/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py +++ b/src/sentry/integrations/github/tasks/sync_repos_on_install_change.py @@ -8,7 +8,6 @@ from sentry.integrations.github.webhook_types import GitHubInstallationRepo from sentry.integrations.services.integration import integration_service from sentry.integrations.services.integration.model import RpcIntegration -from sentry.integrations.services.repository.model import RpcRepository from sentry.integrations.services.repository.service import repository_service from sentry.integrations.source_code_management.metrics import ( SCMIntegrationInteractionEvent, @@ -18,7 +17,6 @@ from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import RpcOrganization from sentry.plugins.providers.integration_repository import ( - RepoExistsError, RepositoryInputConfig, get_integration_repository_provider, ) @@ -38,7 +36,7 @@ processing_deadline_duration=120, silo_mode=SiloMode.CONTROL, ) -@retry(exclude=(RepoExistsError, KeyError)) +@retry(exclude=(KeyError,)) def sync_repos_on_install_change( integration_id: int, action: str, @@ -121,14 +119,11 @@ def _sync_repos_for_org( continue if repo_configs: - created_repos: list[RpcRepository] = [] - reactivated_repos: list[RpcRepository] = [] - try: - created_repos, reactivated_repos = integration_repo_provider.create_repositories( + created_repos, reactivated_repos, _missing_repos = ( + integration_repo_provider.create_repositories( configs=repo_configs, organization=rpc_org ) - except RepoExistsError: - pass + ) for created_repo in created_repos: log_repo_change( diff --git a/src/sentry/plugins/providers/integration_repository.py b/src/sentry/plugins/providers/integration_repository.py index 20170a4f567c05..2834592c069e25 100644 --- a/src/sentry/plugins/providers/integration_repository.py +++ b/src/sentry/plugins/providers/integration_repository.py @@ -240,11 +240,13 @@ def create_repositories( self, configs: list[RepositoryInputConfig], organization: RpcOrganization, - ) -> tuple[list[RpcRepository], list[RpcRepository]]: + ) -> tuple[list[RpcRepository], list[RpcRepository], list[RepositoryConfig]]: """ Create or update repositories from configs. - Returns (created, reactivated) — newly created repos and repos that - were reactivated or updated from a hidden/unlinked state. + Returns (created, reactivated, missing) — newly created repos, repos that + were reactivated or updated from a hidden/unlinked state, and repo configs + that could not be created because a repository with that configuration + already exists. """ external_id_to_repo_config: dict[str, RepositoryConfig] = {} for config in configs: @@ -306,10 +308,7 @@ def create_repositories( updates=repos_to_update, ) - if missing_repos: - raise RepoExistsError(repos=missing_repos) - - return created_repos, repos_to_update + return created_repos, repos_to_update, missing_repos def dispatch(self, request: Request, organization, **kwargs): try: