Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
14 changes: 14 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,10 @@
from sentry.seer.endpoints.organization_seer_workflows import OrganizationSeerWorkflowsEndpoint
from sentry.seer.endpoints.project_seer_night_shift import ProjectSeerNightShiftEndpoint
from sentry.seer.endpoints.project_seer_preferences import ProjectSeerPreferencesEndpoint
from sentry.seer.endpoints.project_seer_repos import (
OrganizationSeerProjectRepoDetailsEndpoint,
OrganizationSeerProjectReposEndpoint,
)
from sentry.seer.endpoints.project_seer_settings import (
OrganizationSeerProjectSettingsEndpoint,
ProjectSeerSettingsEndpoint,
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.
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
114 changes: 114 additions & 0 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,120 @@
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 replace_all_branch_overrides(
project_repo: SeerProjectRepository, branch_overrides: list[BranchOverrideData]
) -> None:
"""Replace all branch overrides for the given Seer project repo."""
SeerProjectRepositoryBranchOverride.objects.filter(
seer_project_repository=project_repo
).delete()
if branch_overrides:
SeerProjectRepositoryBranchOverride.objects.bulk_create(
[
SeerProjectRepositoryBranchOverride(
seer_project_repository=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, repos_data: list[ProjectRepoCreateData]) -> list[int]:
"""Upsert repos for the given project. Creates new connections or updates existing ones."""
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, repos_data: list[ProjectRepoCreateData]
) -> None:
"""Replace all repos for the given project."""
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
list(Project.objects.select_for_update().filter(id=project.id))
SeerProjectRepository.objects.filter(project_repository__project=project).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"),
instructions=data.get("instructions"),
)

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

View check run for this annotation

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

IntegrityError on duplicate repository_id in replace_all_seer_project_repos

If `repos_data` contains duplicate `repository_id` values (not validated by the endpoint), `get_or_create` returns the same `ProjectRepository` on the second iteration and `SeerProjectRepository.objects.create(project_repository=project_repo)` raises an unhandled `IntegrityError` because `SeerProjectRepository.project_repository` has `unique=True`.
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 project repo under a row lock. 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