Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4855a01
add new connected repos endpoint
srest2021 May 8, 2026
9d3ad42
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] May 8, 2026
023e2d6
move helpers to uitls and add tests
srest2021 May 8, 2026
0c3e1bb
fix bugs and add endpoint tests
srest2021 May 8, 2026
72243e7
cleaning up
srest2021 May 8, 2026
217e3bf
resolve merge conflicts
srest2021 May 19, 2026
9ffb9e8
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] May 19, 2026
65cb9ce
fix tests, link projectrepository, remove unnecessary queries
srest2021 May 19, 2026
580e22a
fix comments
srest2021 May 19, 2026
55a41ed
fix tests
srest2021 May 19, 2026
5196d7a
fix mypy
srest2021 May 19, 2026
9a769e4
rename file
srest2021 May 20, 2026
251463a
bug fixes and cleaning up
srest2021 May 20, 2026
63ad438
reorder endpoints
srest2021 May 20, 2026
a680213
fix missing arg
srest2021 May 20, 2026
f0b3791
feat(seer): Add CRUD helpers for Seer project repos
srest2021 May 20, 2026
69e411d
fix validation
srest2021 May 20, 2026
b778898
more test coverage
srest2021 May 20, 2026
e0e8350
remove validation fn from this pr
srest2021 May 20, 2026
4bc027b
fix get_or_create
srest2021 May 20, 2026
6d7bb23
remove project lock for replace-all
srest2021 May 20, 2026
d1f2493
resolve merge conflicts
srest2021 May 20, 2026
279d7b8
require repo id when creating
srest2021 May 20, 2026
20ea2df
Merge branch 'srest2021/CW-1286-utils' into srest2021/CW-1286
srest2021 May 20, 2026
47572b0
resolve merge conflicts, split to details-only endpoint
srest2021 May 20, 2026
177eaf4
remove update fn to inline it in the endpoint
srest2021 May 20, 2026
9784314
Merge branch 'srest2021/CW-1286-utils' into srest2021/CW-1286
srest2021 May 20, 2026
5462ec8
inline helper and move tests
srest2021 May 20, 2026
1d89266
ref base queryset
srest2021 May 20, 2026
6dfb8dd
ref base queryset
srest2021 May 20, 2026
686eeec
Merge branch 'master' into srest2021/CW-1286
srest2021 May 20, 2026
2e93fc6
ensure at least one update field provided
srest2021 May 20, 2026
4e49993
add branch override id to response
srest2021 May 20, 2026
662db71
add org id to project repo response
srest2021 May 20, 2026
4d73e36
only save projectrepo if something changed
srest2021 May 20, 2026
ab0c1dd
more test coverage
srest2021 May 20, 2026
06766c7
Accidentally added test script
srest2021 May 20, 2026
3f9354a
simplify unsupported provider tests
srest2021 May 20, 2026
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
8 changes: 8 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,9 @@
OrganizationSeerAgentUpdateEndpoint,
)
from sentry.seer.endpoints.organization_seer_onboarding_check import OrganizationSeerOnboardingCheck
from sentry.seer.endpoints.organization_seer_project_repos import (
OrganizationSeerProjectRepoDetailsEndpoint,
)
from sentry.seer.endpoints.organization_seer_rpc import OrganizationSeerRpcEndpoint
from sentry.seer.endpoints.organization_seer_setup_check import OrganizationSeerSetupCheckEndpoint
from sentry.seer.endpoints.organization_seer_workflows import OrganizationSeerWorkflowsEndpoint
Expand Down Expand Up @@ -2449,6 +2452,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationSeerOnboardingCheck.as_view(),
name="sentry-api-0-organization-seer-onboarding-check",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/seer/projects/(?P<project_id>\d+)/repos/(?P<repo_id>\d+)/$",
Comment thread
srest2021 marked this conversation as resolved.
OrganizationSeerProjectRepoDetailsEndpoint.as_view(),
name="sentry-api-0-organization-seer-project-repo-details",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/autofix/automation-settings/$",
OrganizationAutofixAutomationSettingsEndpoint.as_view(),
Expand Down
190 changes: 190 additions & 0 deletions src/sentry/seer/endpoints/organization_seer_project_repos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
from __future__ import annotations

from typing import TypedDict

from django.db import router, transaction
from rest_framework import serializers
from rest_framework.request import Request
from rest_framework.response import Response

from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
from sentry.constants import ObjectStatus
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.seer.autofix.utils import replace_all_branch_overrides
from sentry.seer.models.project_repository import SeerProjectRepository
from sentry.seer.seer_setup import get_supported_scm_providers


class BranchOverrideResponse(TypedDict):
id: str
tagName: str
tagValue: str
branchName: str


class ProjectRepoResponse(TypedDict):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the current SeerRepoDefinition built in the read path via build_repo_definition_from_project_repo.

id: str
repositoryId: str
organizationId: str
provider: str
owner: str
name: str
externalId: str
integrationId: str | None
branchName: str | None
branchOverrides: list[BranchOverrideResponse]
instructions: str | None


def _serialize_project_repo(project_repo: SeerProjectRepository) -> ProjectRepoResponse:
repo = project_repo.project_repository.repository
name_parts = repo.name.split("/", 1)
owner = name_parts[0] if len(name_parts) > 1 else ""
name = name_parts[1] if len(name_parts) > 1 else repo.name

return ProjectRepoResponse(
id=str(project_repo.id),
repositoryId=str(repo.id),
organizationId=str(repo.organization_id),
provider=repo.provider or "",
owner=owner,
name=name,
externalId=repo.external_id or "",
integrationId=str(repo.integration_id) if repo.integration_id is not None else None,
branchName=project_repo.branch_name,
branchOverrides=[
BranchOverrideResponse(
id=str(bo.id),
tagName=bo.tag_name,
tagValue=bo.tag_value,
branchName=bo.branch_name,
)
for bo in project_repo.branch_overrides.all()
],
instructions=project_repo.instructions,
)


def _valid_project_repos_queryset(project: Project, organization: Organization):
"""Get a project's base SeerProjectRepository queryset for active repos with providers supported by Seer."""
return SeerProjectRepository.objects.filter(
project_repository__project=project,
project_repository__repository__status=ObjectStatus.ACTIVE,
project_repository__repository__provider__in=get_supported_scm_providers(organization),
).select_related("project_repository", "project_repository__repository")


def _validate_branch_overrides(value):
if not value:
return value
seen: set[tuple[str, str]] = set()
for override in value:
key = (override["tag_name"], override["tag_value"])
if key in seen:
raise serializers.ValidationError(
f"Duplicate branch override for tag {key[0]}={key[1]}"
)
seen.add(key)
return value


class BranchOverrideSerializer(CamelSnakeSerializer):
tag_name = serializers.CharField(required=True)
tag_value = serializers.CharField(required=True)
branch_name = serializers.CharField(required=True)


class SeerProjectRepoUpdateSerializer(CamelSnakeSerializer):
branch_name = serializers.CharField(required=False, allow_null=True, allow_blank=True)
instructions = serializers.CharField(required=False, allow_null=True, allow_blank=True)
branch_overrides = BranchOverrideSerializer(many=True, required=False, allow_null=False)

def validate_branch_overrides(self, value):
return _validate_branch_overrides(value)

def validate(self, data):
if not data:
raise serializers.ValidationError("At least one field must be provided.")
return data


@cell_silo_endpoint
class OrganizationSeerProjectRepoDetailsEndpoint(OrganizationEndpoint):
owner = ApiOwner.ML_AI
publish_status = {
"GET": ApiPublishStatus.EXPERIMENTAL,
"PUT": ApiPublishStatus.EXPERIMENTAL,
"DELETE": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (OrganizationPermission,)

def get(
self, request: Request, organization: Organization, project_id: int, repo_id: int
) -> Response:
project = self.get_projects(request, organization, project_ids={int(project_id)})[0]

project_repo = (
_valid_project_repos_queryset(project, organization)
.prefetch_related("branch_overrides")
.filter(project_repository__repository_id=repo_id)
.first()
)
Comment thread
srest2021 marked this conversation as resolved.
if project_repo is None:
return Response(status=404)

return Response(_serialize_project_repo(project_repo))

def put(
self, request: Request, organization: Organization, project_id: int, repo_id: int
) -> Response:
project = self.get_projects(request, organization, project_ids={int(project_id)})[0]

serializer = SeerProjectRepoUpdateSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=400)

data = serializer.validated_data

with transaction.atomic(router.db_for_write(SeerProjectRepository)):
project_repo = (
_valid_project_repos_queryset(project, organization)
.select_for_update()
.filter(project_repository__repository_id=repo_id)
.first()
)
if project_repo is None:
return Response(status=404)

if "branch_name" in data:
project_repo.branch_name = data["branch_name"]
if "instructions" in data:
project_repo.instructions = data["instructions"]
if "branch_name" in data or "instructions" in data:
project_repo.save()

if "branch_overrides" in data:
replace_all_branch_overrides(project_repo, data["branch_overrides"])

return Response(_serialize_project_repo(project_repo))

def delete(
self, request: Request, organization: Organization, project_id: int, repo_id: int
) -> Response:
project = self.get_projects(request, organization, project_ids={int(project_id)})[0]
Comment thread
srest2021 marked this conversation as resolved.

with transaction.atomic(router.db_for_write(SeerProjectRepository)):
deleted_count, _ = (
_valid_project_repos_queryset(project, organization)
.filter(project_repository__repository_id=repo_id)
.delete()
)

if deleted_count == 0:
return Response(status=404)

return Response(status=204)
1 change: 1 addition & 0 deletions static/app/utils/api/knownSentryApiUrls.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ export type KnownSentryApiUrls =
| '/organizations/$organizationIdOrSlug/seer/explorer-update/$runId/'
| '/organizations/$organizationIdOrSlug/seer/onboarding-check/'
| '/organizations/$organizationIdOrSlug/seer/projects/'
| '/organizations/$organizationIdOrSlug/seer/projects/$projectId/repos/$repoId/'
| '/organizations/$organizationIdOrSlug/seer/setup-check/'
| '/organizations/$organizationIdOrSlug/seer/supergroups/$supergroupId/'
| '/organizations/$organizationIdOrSlug/seer/supergroups/by-group/'
Expand Down
Loading
Loading