Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
42 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
5785d94
switch to project-scoped endpoint
srest2021 May 21, 2026
e7186a6
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] May 21, 2026
0cb1eaf
rename endpoint
srest2021 May 21, 2026
6c08ae9
add cross-project test coverage
srest2021 May 21, 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
14 changes: 14 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@
OrganizationSeerAgentUpdateEndpoint,
)
from sentry.seer.endpoints.organization_seer_onboarding_check import OrganizationSeerOnboardingCheck
from sentry.seer.endpoints.organization_seer_project_repos import (
OrganizationSeerProjectRepoDetailsEndpoint,
OrganizationSeerProjectReposEndpoint,
)
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 @@ -2455,6 +2459,16 @@ 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/$",
OrganizationSeerProjectReposEndpoint.as_view(),
name="sentry-api-0-organization-seer-project-repos",
),
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.
Outdated
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
145 changes: 145 additions & 0 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,151 @@
ProjectOption.objects.reload_cache(project_id, "projectoption.bulk_set_value")


class BranchOverrideData(TypedDict):
tag_name: str
tag_value: str
branch_name: str


class ProjectRepoCreateData(TypedDict, total=False):
repository_id: int
branch_name: str | None
instructions: str | None
branch_overrides: list[BranchOverrideData]


def _validate_repo_ids(
organization: Organization,
repo_ids: list[int],
) -> None:
"""Raise ValueError if any repo IDs are invalid, inactive, or unsupported."""
valid_ids = set(
Repository.objects.filter(
id__in=repo_ids,
organization_id=organization.id,
status=ObjectStatus.ACTIVE,
provider__in=get_supported_scm_providers(),
).values_list("id", flat=True)
)
invalid_ids = set(repo_ids) - valid_ids
if invalid_ids:
Comment thread
srest2021 marked this conversation as resolved.
Outdated
raise ValueError(sorted(invalid_ids))


def replace_all_branch_overrides(
seer_project_repo: SeerProjectRepository, branch_overrides: list[BranchOverrideData]
) -> None:
"""Replace all branch overrides for the given Seer project repo."""
SeerProjectRepositoryBranchOverride.objects.filter(
seer_project_repository=seer_project_repo
).delete()
if branch_overrides:
SeerProjectRepositoryBranchOverride.objects.bulk_create(
[
SeerProjectRepositoryBranchOverride(
seer_project_repository=seer_project_repo,
tag_name=override["tag_name"],
tag_value=override["tag_value"],
branch_name=override["branch_name"],
)
for override in branch_overrides
]
)


def add_seer_project_repos(
project: Project, organization: Organization, repos_data: list[ProjectRepoCreateData]
) -> list[int]:
"""Upsert Seer project repos. Raises ValueError if any repo IDs are invalid."""
_validate_repo_ids(organization, [d["repository_id"] for d in repos_data])

result_ids = []
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
for data in repos_data:
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
repository_id=data["repository_id"],
defaults={"source": ProjectRepositorySource.SEER_PREFERENCE},
)
seer_project_repo, _ = SeerProjectRepository.objects.update_or_create(
project_repository=project_repo,
defaults={
"branch_name": data.get("branch_name"),
"instructions": data.get("instructions"),
},
)
replace_all_branch_overrides(seer_project_repo, data.get("branch_overrides", []))
result_ids.append(seer_project_repo.id)

return result_ids


def replace_all_seer_project_repos(
project: Project, organization: Organization, repos_data: list[ProjectRepoCreateData]
) -> None:
"""Replace all active Seer repos for the given project.
Raises ValueError if any repo IDs are invalid."""
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
list(Project.objects.select_for_update().filter(id=project.id))

if repos_data:
_validate_repo_ids(organization, [d["repository_id"] for d in repos_data])

SeerProjectRepository.objects.filter(
project_repository__project=project,
project_repository__repository__status=ObjectStatus.ACTIVE,
).delete()

for data in repos_data:
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
repository_id=data["repository_id"],
defaults={"source": ProjectRepositorySource.SEER_PREFERENCE},
)
seer_project_repo = SeerProjectRepository.objects.create(
project_repository=project_repo,
branch_name=data.get("branch_name"),

Check failure on line 960 in src/sentry/seer/autofix/utils.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

IntegrityError in replace_all_seer_project_repos when repos_data contains duplicate repository_id values

If a PUT request includes the same `repositoryId` twice, the second `SeerProjectRepository.objects.create()` call will hit the `unique=True` constraint on `project_repository`, raising an unhandled `IntegrityError` and returning a 500.
instructions=data.get("instructions"),
)
Comment thread
sentry-warden[bot] marked this conversation as resolved.
Outdated
replace_all_branch_overrides(seer_project_repo, data.get("branch_overrides", []))
Comment thread
srest2021 marked this conversation as resolved.
Outdated


class ProjectRepoUpdateData(TypedDict, total=False):
branch_name: str | None
instructions: str | None
branch_overrides: list[BranchOverrideData]


def update_seer_project_repo(
project: Project, repo_id: int, data: ProjectRepoUpdateData
) -> SeerProjectRepository | None:
"""Update a Seer project repo. Returns None if not found."""
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
project_repo = (
SeerProjectRepository.objects.select_for_update()
.select_related("project_repository", "project_repository__repository")
.filter(
project_repository__project=project,
project_repository__repository_id=repo_id,
project_repository__repository__status=ObjectStatus.ACTIVE,
)
.first()
)
if project_repo is None:
return None

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

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

return project_repo


def has_project_connected_repos(organization: Organization, project: Project) -> bool:
"""Check if a project has connected repositories for Seer automation."""
return SeerProjectRepository.objects.filter(
Expand Down
Loading
Loading