From b6656d867e48c3353fb7fdff575e5abb231fbe6b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:15:41 +0000 Subject: [PATCH 1/2] ci(selective): path-filter endpoint edits into Seer public API matrix Force-include the Seer public-API matrix when changed files match src/sentry/**/endpoints/**.py so publish_status flips cannot skip it. Co-Authored-By: Dan Fuller --- .../scripts/compute-sentry-selected-tests.py | 24 +++++++ .../test_compute_sentry_selected_tests.py | 69 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/compute-sentry-selected-tests.py b/.github/workflows/scripts/compute-sentry-selected-tests.py index d790a3103d1c..4066423106d1 100644 --- a/.github/workflows/scripts/compute-sentry-selected-tests.py +++ b/.github/workflows/scripts/compute-sentry-selected-tests.py @@ -157,6 +157,14 @@ "tests/sentry/backup/test_validate.py", } +# Seer public-API matrix discovers PUBLIC mutations at collection time, so +# endpoint module edits (including publish_status flips) need an explicit include. +PUBLIC_API_MATRIX_TEST = "tests/sentry/seer/endpoints/test_organization_agent_token.py" +PUBLIC_API_MATRIX_PATH_TRIGGERS: list[re.Pattern[str]] = [ + # Endpoint modules live under */endpoints/ across product areas. + re.compile(r"^src/sentry/.*/endpoints/.*\.py$"), +] + def _is_test(path: str) -> bool: return any(path.startswith(d) for d in TEST_DIRS) @@ -168,6 +176,14 @@ def _matches_trigger(file_path: str, trigger: str | re.Pattern[str]) -> bool: return file_path == trigger +def _changed_files_match_public_api_matrix_paths(changed_files: list[str]) -> list[str]: + return [ + f + for f in changed_files + if any(_matches_trigger(f, t) for t in PUBLIC_API_MATRIX_PATH_TRIGGERS) + ] + + def _query_coverage(coverage_db_path: str, db_file_paths: list[str]) -> set[str]: """Query coverage DB for test contexts covering the given source files.""" conn = sqlite3.connect(coverage_db_path) @@ -313,6 +329,14 @@ def main() -> int: # Always run these tests affected_test_files.update(ALWAYS_RUN_TESTS) + endpoint_sources = _changed_files_match_public_api_matrix_paths(changed) + if endpoint_sources: + print( + "Including public API matrix test due to endpoint path(s): " + + ", ".join(endpoint_sources) + ) + affected_test_files.add(PUBLIC_API_MATRIX_TEST) + # Filter to sentry tests only (drop any getsentry tests from coverage) affected_test_files = {f for f in affected_test_files if _is_test(f)} diff --git a/.github/workflows/scripts/test_compute_sentry_selected_tests.py b/.github/workflows/scripts/test_compute_sentry_selected_tests.py index e9336f0037d0..937ab8047c13 100644 --- a/.github/workflows/scripts/test_compute_sentry_selected_tests.py +++ b/.github/workflows/scripts/test_compute_sentry_selected_tests.py @@ -24,6 +24,8 @@ EXTRA_DIR_TO_TEST_MAPPING, EXTRA_FILE_TO_TEST_MAPPING, FULL_SUITE_TRIGGERS, + PUBLIC_API_MATRIX_TEST, + _changed_files_match_public_api_matrix_paths, _query_coverage, main, ) @@ -427,6 +429,71 @@ def test_missing_db_returns_error(self): ret = _run(["--coverage-db", "/nonexistent/coverage.db", "--changed-files", "foo.py"]) assert ret == 1 + def test_endpoint_path_force_includes_public_api_matrix(self, tmp_path): + db_path = tmp_path / "coverage.db" + _create_coverage_db(str(db_path), {}) + output = tmp_path / "output.txt" + gh_output = tmp_path / "gh_output" + gh_output.write_text("") + + with mock.patch("compute_sentry_selected_tests.Path.exists", return_value=True): + _run( + [ + "--coverage-db", + str(db_path), + "--changed-files", + "src/sentry/api/endpoints/views.py", + "--output", + str(output), + "--github-output", + ], + {"GITHUB_OUTPUT": str(gh_output)}, + ) + + selected = set(output.read_text().splitlines()) + assert PUBLIC_API_MATRIX_TEST in selected + assert selected == ALWAYS_RUN_TESTS | {PUBLIC_API_MATRIX_TEST} + assert "has-selected-tests=true" in gh_output.read_text() + + def test_non_endpoint_source_does_not_force_public_api_matrix(self, tmp_path): + db_path = tmp_path / "coverage.db" + _create_coverage_db(str(db_path), {}) + output = tmp_path / "output.txt" + gh_output = tmp_path / "gh_output" + gh_output.write_text("") + + with mock.patch("compute_sentry_selected_tests.Path.exists", return_value=True): + _run( + [ + "--coverage-db", + str(db_path), + "--changed-files", + "src/sentry/utils/thing.py", + "--output", + str(output), + "--github-output", + ], + {"GITHUB_OUTPUT": str(gh_output)}, + ) + + assert set(output.read_text().splitlines()) == ALWAYS_RUN_TESTS + assert PUBLIC_API_MATRIX_TEST not in output.read_text() + + +class TestPublicApiMatrixPathTriggers: + def test_matches_endpoint_paths(self): + assert _changed_files_match_public_api_matrix_paths( + [ + "src/sentry/api/endpoints/views.py", + "src/sentry/issues/endpoints/organization_group_search_views.py", + "src/sentry/utils/thing.py", + "tests/sentry/api/endpoints/test_views.py", + ] + ) == [ + "src/sentry/api/endpoints/views.py", + "src/sentry/issues/endpoints/organization_group_search_views.py", + ] + class TestConfigPaths: """Assert every literal path in the selective testing config still exists on disk. @@ -440,7 +507,7 @@ class TestConfigPaths: def test_full_suite_triggers_exist(self, trigger: str) -> None: assert (_REPO_ROOT / trigger).exists(), _stale_msg(trigger) - @pytest.mark.parametrize("path", sorted(ALWAYS_RUN_TESTS)) + @pytest.mark.parametrize("path", sorted(ALWAYS_RUN_TESTS | {PUBLIC_API_MATRIX_TEST})) def test_always_run_tests_exist(self, path: str) -> None: assert (_REPO_ROOT / path).exists(), _stale_msg(path, "test file") From 96e9977bebd28349ffeba280c9f05a6f3b265a24 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:15:41 +0000 Subject: [PATCH 2/2] Reapply "feat(api): Publish issue view listing endpoint (#122605)" This reverts commit cc1dbf8d51fccd4c9b291a0638aca1265e1e998a. Co-Authored-By: Dan Fuller --- .../organization_group_search_views.py | 56 +++++++++++++++---- .../events/test_organization_issue_views.py | 21 +++++-- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index ab0313bae1d5..93d1543e2a77 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -26,7 +26,7 @@ RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED, ) -from sentry.apidocs.parameters import GlobalParams +from sentry.apidocs.parameters import CursorQueryParam, GlobalParams from sentry.apidocs.response_types import ValidationErrorResponse, as_validation_errors from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewVisibility @@ -73,13 +73,18 @@ class OrganizationGroupSearchViewGetSerializer(serializers.Serializer[None]): createdBy = serializers.ChoiceField( choices=("me", "others"), required=False, + help_text="Whether to return issue views created by the current user or other members.", ) sort = serializers.ListField( child=serializers.ChoiceField(choices=list(SORT_MAP.keys())), required=False, default=["-visited"], + help_text="The fields used to sort issue views, in order of precedence.", + ) + query = serializers.CharField( + required=False, + help_text="A case-insensitive search against issue view names and queries.", ) - query = serializers.CharField(required=False) def validate_query(self, value: str | None) -> str | None: return value.strip() if value else None @@ -89,13 +94,37 @@ def validate_query(self, value: str | None) -> str | None: @cell_silo_endpoint class OrganizationGroupSearchViewsEndpoint(OrganizationEndpoint): publish_status = { - "GET": ApiPublishStatus.EXPERIMENTAL, + "GET": ApiPublishStatus.PUBLIC, "POST": ApiPublishStatus.PUBLIC, } owner = ApiOwner.ISSUES permission_classes = (MemberPermission,) - def get(self, request: Request, organization: Organization) -> Response: + @extend_schema( + operation_id="listOrganizationIssueViews", + summary="List Issue Views", + parameters=[ + GlobalParams.ORG_ID_OR_SLUG, + OrganizationGroupSearchViewGetSerializer, + CursorQueryParam, + ], + responses={ + 200: inline_sentry_response_serializer( + "OrganizationIssueViewList", list[GroupSearchViewSerializerResponse] + ), + 400: RESPONSE_BAD_REQUEST, + 401: RESPONSE_UNAUTHORIZED, + 403: RESPONSE_FORBIDDEN, + 404: RESPONSE_NOT_FOUND, + }, + ) + def get( + self, request: Request, organization: Organization + ) -> ( + Response[list[GroupSearchViewSerializerResponse]] + | Response[ValidationErrorResponse] + | Response[None] + ): """ List the current organization member's custom views ````````````````````````````````````````` @@ -107,7 +136,7 @@ def get(self, request: Request, organization: Organization) -> Response: serializer = OrganizationGroupSearchViewGetSerializer(data=request.GET) if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(as_validation_errors(serializer), status=status.HTTP_400_BAD_REQUEST) starred_view_ids = GroupSearchViewStarred.objects.filter( organization=organization, user_id=request.user.id @@ -177,17 +206,20 @@ def get(self, request: Request, organization: Organization) -> Response: else: raise ValueError(f"Unexpected createdBy value: {createdBy}") + def serialize_views( + views: list[GroupSearchView], + ) -> list[GroupSearchViewSerializerResponse]: + return serialize( + views, + request.user, + serializer=GroupSearchViewSerializer(organization=organization), + ) + return self.paginate( request=request, sources=[starred_query, non_starred_query], paginator_cls=ChainPaginator, - on_results=lambda x: serialize( - x, - request.user, - serializer=GroupSearchViewSerializer( - organization=organization, - ), - ), + on_results=serialize_views, ) @extend_schema( diff --git a/tests/apidocs/endpoints/events/test_organization_issue_views.py b/tests/apidocs/endpoints/events/test_organization_issue_views.py index e81673e4bbf8..4ef525e540bc 100644 --- a/tests/apidocs/endpoints/events/test_organization_issue_views.py +++ b/tests/apidocs/endpoints/events/test_organization_issue_views.py @@ -8,10 +8,7 @@ class OrganizationIssueViewsDocs(APIDocsTestCase): def setUp(self) -> None: self.login_as(user=self.user) self.url = f"/api/0/organizations/{self.organization.slug}/group-search-views/" - - @with_feature({"organizations:issue-views": True}) - def test_post(self) -> None: - data = { + self.data = { "name": "My Issues", "query": "is:unresolved", "querySort": "date", @@ -20,7 +17,19 @@ def test_post(self) -> None: "timeFilters": {"period": "14d"}, } - response = self.client.post(self.url, data) - request = RequestFactory().post(self.url, data) + @with_feature({"organizations:issue-views": True}) + def test_get(self) -> None: + create_response = self.client.post(self.url, self.data) + assert create_response.status_code == 201 + + response = self.client.get(self.url) + request = RequestFactory().get(self.url) + + self.validate_schema(request, response) + + @with_feature({"organizations:issue-views": True}) + def test_post(self) -> None: + response = self.client.post(self.url, self.data) + request = RequestFactory().post(self.url, self.data) self.validate_schema(request, response)