Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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,8 +2459,18 @@
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/$",

Check failure on line 2473 in src/sentry/api/urls.py

View check run for this annotation

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

New Seer project-repos endpoints use removed ORM fields, causing FieldError on every request

`SeerProjectRepository` no longer has `project` or `repository` FK fields — they were removed by migration `0016_remove_old_fks` (SafeRemoveField with MOVE_TO_PENDING removes the fields from the Django model state). The current model in `src/sentry/seer/models/project_repository.py` only declares the `project_repository` FK to `sentry.ProjectRepository`. However, the new endpoints and helpers introduced in this PR repeatedly reference the removed fields: - `src/sentry/seer/endpoints/project_seer_repos.py`: - `_get_project_repos_queryset` (≈ line 132): `SeerProjectRepository.objects.filter(project=project, repository__status=ObjectStatus.ACTIVE).select_related("repository")` and annotation `Replace("repository__provider", ...)`. - `_serialize_project_repo` (≈ line 67): `project_repo.repository` attribute access. - `_get_project_repo` / `OrganizationSeerProjectRepoDetailsEndpoint.delete` (≈ line 332): `.filter(project=project, repository_id=repo_id, repository__status=...)`. - `src/sentry/seer/autofix/utils.py`: - `add_seer_project_repos` (≈ line 891): `.filter(project=project, repository_id__in=repo_ids)` and `SeerProjectRepository.objects.create(project=project, repository_id=...)`. - `replace_all_seer_project_repos` (≈ line 916): `.filter(project=project).delete()` and `.create(project=project, repository_id=...)`. Existing working code (e.g. `read_preference_from_sentry_db`, `has_project_connected_repos`) correctly traverses `project_repository__project=...` and `project_repository__repository__status=...`. The new endpoints/helpers will raise `django.core.exceptions.FieldError: Cannot resolve keyword 'project' into field` (and `repository`) on every GET/POST/PUT/DELETE, and `_serialize_project_repo` will additionally raise `AttributeError` because `repository` is no longer a direct relation. Fix: replace `project=project` with `project_repository__project=project`, `repository_id=...`/`repository_id__in=...` with `project_repository__repository_id=...`/`__in=...`, `repository__status=...` with `project_repository__repository__status=...`, `repository__provider`/`repository__name` with `project_repository__repository__provider`/`__name`, and access `project_repo.project_repository.repository` in `_serialize_project_repo`. For `create`/`delete`, construct/select via the `ProjectRepository` link (e.g. look up or create the `ProjectRepository` row and pass `project_repository=<pr>`).
OrganizationAutofixAutomationSettingsEndpoint.as_view(),
name="sentry-api-0-organization-autofix-automation-settings",
),
Expand Down
101 changes: 101 additions & 0 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,107 @@
ProjectOption.objects.reload_cache(project_id, "projectoption.bulk_set_value")


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


def _write_branch_overrides(
project_repo: SeerProjectRepository, branch_overrides: list[BranchOverrideData]
) -> None:
"""Replace all branch overrides for the given 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
]
)


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


def add_seer_project_repos(project: Project, repos_data: list[ProjectRepoCreateData]) -> list[int]:
"""Connect repos to the given project. Raises ValueError if any repo is already connected."""
repo_ids = [d["repository_id"] for d in repos_data]

connected_ids = set(
SeerProjectRepository.objects.filter(
project=project, repository_id__in=repo_ids
).values_list("repository_id", flat=True)
)
if connected_ids:
raise ValueError(connected_ids)

created_ids = []
with transaction.atomic(router.db_for_write(SeerProjectRepository)):

Check warning on line 904 in src/sentry/seer/autofix/utils.py

View check run for this annotation

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

Duplicate-repo check runs outside the transaction, causing unhandled IntegrityError under concurrent requests

The `connected_ids` duplicate check (lines 895–901) is performed before the transaction begins, so two concurrent requests for the same project+repo IDs can both pass the check; the second `SeerProjectRepository.objects.create()` then violates the unique constraint and raises an unhandled `IntegrityError` (500). Move the check inside the transaction after `select_for_update`.
Comment thread
srest2021 marked this conversation as resolved.
Outdated
list(Project.objects.select_for_update().filter(id=project.id))

for data in repos_data:
project_repo = SeerProjectRepository.objects.create(
project=project,
repository_id=data["repository_id"],
branch_name=data.get("branch_name"),
instructions=data.get("instructions"),
)
_write_branch_overrides(project_repo, data.get("branch_overrides", []))
created_ids.append(project_repo.id)

return created_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=project).delete()
for data in repos_data:
project_repo = SeerProjectRepository.objects.create(
project=project,
repository_id=data["repository_id"],
branch_name=data.get("branch_name"),
instructions=data.get("instructions"),
)
_write_branch_overrides(project_repo, data.get("branch_overrides", []))


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


def update_seer_project_repo(
project_repo: SeerProjectRepository, data: ProjectRepoUpdateData
) -> None:
"""Update a given project repo. Raises DatabaseError if the row doesn't exist by the time we save."""
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
list(Project.objects.select_for_update().filter(id=project_repo.project_id))
if "branch_name" in data:
project_repo.branch_name = data["branch_name"]
if "instructions" in data:
project_repo.instructions = data["instructions"]
project_repo.save(force_update=True)
if "branch_overrides" in data:
_write_branch_overrides(project_repo, data["branch_overrides"])


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