Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/sentry/integrations/cursor_origin/repository_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,34 @@
from collections.abc import Mapping, Sequence
from typing import Any

from sentry.constants import ObjectStatus
from sentry.integrations.cursor_origin.constants import CURSOR_ORIGIN_WEB_BASE_URL
from sentry.integrations.cursor_origin.handlers import WebhookEventHandler
from sentry.integrations.cursor_origin.repository import active_repositories
from sentry.integrations.cursor_origin.webhook_types import (
RepositoryDeletedEvent,
RepositoryMetadataEvent,
RepositorySnapshot,
)
from sentry.integrations.services.integration.model import (
RpcIntegration,
RpcOrganizationIntegration,
)
from sentry.integrations.services.repository import repository_service
from sentry.integrations.source_code_management.repo_audit import log_repo_change
from sentry.integrations.source_code_management.sync_repos import DISABLE_ACTIVITY_CUTOFF_DAYS
from sentry.integrations.types import IntegrationProviderSlug
from sentry.integrations.utils.metrics import IntegrationWebhookEventType
from sentry.models.organization import Organization
from sentry.models.repository import Repository
from sentry.organizations.services.organization.serial import serialize_rpc_organization
from sentry.plugins.providers.integration_repository import get_integration_repository_provider
from sentry.utils import metrics

logger = logging.getLogger("sentry.integrations.cursor_origin")

PROVIDER = f"integrations:{IntegrationProviderSlug.CURSOR_ORIGIN.value}"


class RepositoryMetadataUpdatedHandler(WebhookEventHandler):
"""Apply a default-branch change. The name is repaired for every event."""
Expand Down Expand Up @@ -84,3 +96,125 @@ def store_default_branch(repo: Repository, snapshot: RepositorySnapshot, deliver
},
)
repo.update(config={**repo.config, "default_branch": snapshot.default_branch})


def _active_org_integrations(
integration: RpcIntegration,
org_integrations: Sequence[RpcOrganizationIntegration],
delivery_id: str,
) -> list[RpcOrganizationIntegration]:
if integration.status != ObjectStatus.ACTIVE:
logger.info(
"cursor_origin.repository.inactive_integration",
extra={"delivery_id": delivery_id, "integration_id": integration.id},
)
return []
return [oi for oi in org_integrations if oi.status == ObjectStatus.ACTIVE]


class RepositoryCreatedHandler(WebhookEventHandler):
EVENT_TYPE = IntegrationWebhookEventType.INBOUND_SYNC

def __call__(
self,
payload: Mapping[str, Any],
delivery_id: str,
integration: RpcIntegration,
org_integrations: Sequence[RpcOrganizationIntegration],
) -> None:
snapshot = RepositoryMetadataEvent.from_payload(payload).repository
Comment thread
wedamija marked this conversation as resolved.
provider = get_integration_repository_provider(integration)
config = {
"name": snapshot.full_name,
"external_id": snapshot.id,
"default_branch": snapshot.default_branch,
"integration_id": integration.id,
}

org_integrations = _active_org_integrations(integration, org_integrations, delivery_id)
for organization in Organization.objects.filter(
id__in=[oi.organization_id for oi in org_integrations]
):
Comment thread
wedamija marked this conversation as resolved.
created, reactivated, _ = provider.create_repositories(
configs=[config], organization=serialize_rpc_organization(organization)
)
if created:
repository_service.auto_link_repos_by_name(
organization_id=organization.id, repo_ids=[repo.id for repo in created]
)
for repo in created:
log_repo_change(
event_name="REPO_ADDED",
organization_id=organization.id,
repo=repo,
source="Cursor Origin webhook",
provider=integration.provider,
)
for repo in reactivated:
log_repo_change(
event_name="REPO_ENABLED",
organization_id=organization.id,
repo=repo,
source="Cursor Origin webhook",
provider=integration.provider,
)
Comment thread
wedamija marked this conversation as resolved.


class RepositoryDeletedHandler(WebhookEventHandler):
EVENT_TYPE = IntegrationWebhookEventType.INBOUND_SYNC

def __call__(
self,
payload: Mapping[str, Any],
delivery_id: str,
integration: RpcIntegration,
org_integrations: Sequence[RpcOrganizationIntegration],
) -> None:
external_id = RepositoryDeletedEvent.from_payload(payload).repository.id
for org_integration in _active_org_integrations(integration, org_integrations, delivery_id):
organization_id = org_integration.organization_id
if repository_service.find_recently_active_repo_external_ids(
organization_id=organization_id,
integration_id=integration.id,
provider=PROVIDER,
external_ids=[external_id],
cutoff_days=DISABLE_ACTIVITY_CUTOFF_DAYS,
):
logger.info(
"cursor_origin.repository.disable_skipped_due_to_activity",
extra={
"delivery_id": delivery_id,
"organization_id": organization_id,
"external_id": external_id,
"cutoff_days": DISABLE_ACTIVITY_CUTOFF_DAYS,
},
)
metrics.incr(
"cursor_origin.repository.disable_skipped_due_to_activity", sample_rate=1.0
)
continue

active = [
repo
for repo in repository_service.get_repositories(
organization_id=organization_id,
integration_id=integration.id,
providers=[PROVIDER],
external_id=external_id,
)
if repo.status == ObjectStatus.ACTIVE
]
repository_service.disable_repositories_by_external_ids(
organization_id=organization_id,
integration_id=integration.id,
provider=PROVIDER,
external_ids=[external_id],
)
for repo in active:
log_repo_change(
event_name="REPO_DISABLED",
organization_id=organization_id,
repo=repo,
source="Cursor Origin webhook",
provider=integration.provider,
)
4 changes: 4 additions & 0 deletions src/sentry/integrations/cursor_origin/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
from sentry.integrations.cursor_origin.pull_request import PullRequestLifecycleHandler
from sentry.integrations.cursor_origin.push import RepositoryPushedHandler
from sentry.integrations.cursor_origin.repository_events import (
RepositoryCreatedHandler,
RepositoryDeletedHandler,
RepositoryMetadataUpdatedHandler,
refresh_repository_name,
)
Expand Down Expand Up @@ -155,6 +157,8 @@ def verify_delivery(request: HttpRequest, body: bytes) -> Verification:
"pull_request.metadata.updated": PullRequestLifecycleHandler,
"pull_request.published": PullRequestLifecycleHandler,
"pull_request.reopened": PullRequestLifecycleHandler,
"repository.created": RepositoryCreatedHandler,
"repository.deleted": RepositoryDeletedHandler,
"repository.metadata.updated": RepositoryMetadataUpdatedHandler,
"repository.pushed": RepositoryPushedHandler,
}
Expand Down
11 changes: 11 additions & 0 deletions src/sentry/integrations/cursor_origin/webhook_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,14 @@ def from_payload(cls, payload: Mapping[str, Any]) -> InstallationEvent:
return cls.parse_obj(payload)
except ValidationError as e:
raise OriginPayloadError(str(e)) from e


class RepositoryDeletedEvent(OriginModel):
repository: Repository

@classmethod
def from_payload(cls, payload: Mapping[str, Any]) -> RepositoryDeletedEvent:
try:
return cls.parse_obj(payload)
except ValidationError as e:
raise OriginPayloadError(str(e)) from e
15 changes: 10 additions & 5 deletions src/sentry/plugins/providers/integration_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,16 @@ def create_repositories(
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.
already exists. A repo that was already active has its config refreshed but
is not reported as reactivated.
"""
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] = []
refreshed_repos: list[RpcRepository] = []
created_repos: list[RpcRepository] = []
transferred_repos: list[RpcRepository] = []

Expand Down Expand Up @@ -368,7 +370,10 @@ def create_repositories(
missing_repos.append(repo_config)
# We anticipate to only update one repository, but we update any duplicates as well.
for repo in repositories:
repos_to_update.append(self._apply_repo_config(repo, repo_config))
if repo.status == ObjectStatus.ACTIVE:
refreshed_repos.append(self._apply_repo_config(repo, repo_config))
else:
repos_to_update.append(self._apply_repo_config(repo, repo_config))
continue

# if we don't find the repo on this integration, the unique constraint was hit by a
Expand All @@ -394,12 +399,12 @@ def create_repositories(
)
missing_repos.append(repo_config)

if repos_to_update:
if repos_to_update or refreshed_repos:
repository_service.update_repositories(
organization_id=organization.id,
updates=repos_to_update,
updates=repos_to_update + refreshed_repos,
)
for repo in repos_to_update:
for repo in repos_to_update + refreshed_repos:
self.on_create_repository(repo, organization)

return created_repos, repos_to_update + transferred_repos, missing_repos
Expand Down
Loading
Loading