From 59a7d2c257df26395a3ebcd4128b3fcd7f05ebbe Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Wed, 29 Apr 2026 15:43:06 +0200 Subject: [PATCH 1/8] feat(tasks): add summarized tasks endpoint --- products/tasks/backend/api.py | 31 ++++++++++ products/tasks/backend/serializers.py | 33 ++++++++++ products/tasks/backend/tests/test_api.py | 76 ++++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/products/tasks/backend/api.py b/products/tasks/backend/api.py index 711ea4980882..89a611bff5b0 100644 --- a/products/tasks/backend/api.py +++ b/products/tasks/backend/api.py @@ -84,6 +84,8 @@ TaskStagedArtifactsFinalizeUploadResponseSerializer, TaskStagedArtifactsPrepareUploadRequestSerializer, TaskStagedArtifactsPrepareUploadResponseSerializer, + TaskSummariesRequestSerializer, + TaskSummarySerializer, build_task_run_artifact_size_error, get_task_run_artifact_max_size_bytes, ) @@ -212,6 +214,35 @@ def repositories(self, request, **kwargs): serializer = TaskRepositoriesResponseSerializer({"repositories": list(repositories)}) return Response(serializer.data) + @validated_request( + request_serializer=TaskSummariesRequestSerializer, + responses={ + 200: OpenApiResponse(response=TaskSummarySerializer, description="Summary fields for the requested tasks"), + }, + summary="Fetch task summaries by ID", + description=( + "Returns summary for the requested tasks: `id`, `title`, `repository`, `created_at`, " + "`updated_at`, and the latest run's `status` and `environment`." + ), + ) + @action( + detail=False, + methods=["post"], + url_path="summaries", + required_scopes=["task:read"], + pagination_class=None, + filter_backends=[], + ) + def summaries(self, request, **kwargs): + ids = request.validated_data["ids"] + latest_run = TaskRun.objects.filter(task=OuterRef("pk")).order_by("-created_at", "-id") + tasks = Task.objects.filter(team=self.team, deleted=False, id__in=ids).annotate( + _latest_run_status=Subquery(latest_run.values("status")[:1]), + _latest_run_environment=Subquery(latest_run.values("environment")[:1]), + ) + serializer = TaskSummarySerializer(tasks, many=True) + return Response(serializer.data) + @validated_request( query_serializer=RepositoryReadinessQuerySerializer, responses={ diff --git a/products/tasks/backend/serializers.py b/products/tasks/backend/serializers.py index af4695d629a9..db75d5ad18ea 100644 --- a/products/tasks/backend/serializers.py +++ b/products/tasks/backend/serializers.py @@ -748,6 +748,39 @@ class TaskRunArtifactPresignResponseSerializer(serializers.Serializer): expires_in = serializers.IntegerField(help_text="URL expiry in seconds") +TASK_SUMMARIES_MAX_IDS = 500 + + +class TaskSummariesRequestSerializer(serializers.Serializer): + ids = serializers.ListField( + child=serializers.UUIDField(), + allow_empty=False, + max_length=TASK_SUMMARIES_MAX_IDS, + help_text=f"Task IDs to fetch summaries for (max {TASK_SUMMARIES_MAX_IDS}).", + ) + + +class TaskRunSummarySerializer(serializers.Serializer): + status = serializers.CharField(allow_null=True) + environment = serializers.CharField(allow_null=True) + + +class TaskSummarySerializer(serializers.ModelSerializer): + latest_run = serializers.SerializerMethodField() + + class Meta: + model = Task + fields = ["id", "title", "repository", "created_at", "updated_at", "latest_run"] + read_only_fields = fields + + @extend_schema_field(TaskRunSummarySerializer(allow_null=True)) + def get_latest_run(self, obj): + status = getattr(obj, "_latest_run_status", None) + if status is None: + return None + return {"status": status, "environment": getattr(obj, "_latest_run_environment", None)} + + class TaskListQuerySerializer(serializers.Serializer): """Query parameters for listing tasks""" diff --git a/products/tasks/backend/tests/test_api.py b/products/tasks/backend/tests/test_api.py index e49dbe9e21be..a939e9b19ec7 100644 --- a/products/tasks/backend/tests/test_api.py +++ b/products/tasks/backend/tests/test_api.py @@ -1696,6 +1696,82 @@ def test_internal_field_defaults_to_false_on_create(self): self.assertFalse(response.json()["internal"]) +class TestTaskSummariesAPI(BaseTaskAPITest): + SUMMARIES_URL = "/api/projects/@current/tasks/summaries/" + SUMMARY_FIELDS = {"id", "title", "repository", "created_at", "updated_at", "latest_run"} + + def post_summaries(self, ids): + return self.client.post(self.SUMMARIES_URL, {"ids": ids}, format="json") + + def assertReturnsTaskIds(self, response, expected_ids): + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(sorted(t["id"] for t in response.json()), sorted(str(i) for i in expected_ids)) + + def test_summaries_returns_only_requested_ids_scoped_to_team(self): + task_a = self.create_task("Task A") + task_b = self.create_task("Task B") + self.create_task("Task C") + other_team = Team.objects.create(organization=self.organization, name="Other Team") + other_task = Task.objects.create( + team=other_team, title="Other", description="", origin_product=Task.OriginProduct.USER_CREATED + ) + + response = self.post_summaries([str(task_a.id), str(task_b.id), str(other_task.id), str(uuid.uuid4())]) + + self.assertReturnsTaskIds(response, [task_a.id, task_b.id]) + + def test_summaries_returns_internal_tasks_when_requested_by_id(self): + internal_task = Task.objects.create( + team=self.team, + title="Internal", + description="", + origin_product=Task.OriginProduct.USER_CREATED, + internal=True, + ) + + response = self.post_summaries([str(internal_task.id)]) + + self.assertReturnsTaskIds(response, [internal_task.id]) + + @parameterized.expand( + [ + ("with_run", True), + ("no_runs", False), + ] + ) + def test_summaries_response_shape(self, _name, with_run): + task = self.create_task("Task") + run = ( + TaskRun.objects.create( + team=self.team, + task=task, + status=TaskRun.Status.IN_PROGRESS, + environment=TaskRun.Environment.LOCAL, + ) + if with_run + else None + ) + + response = self.post_summaries([str(task.id)]) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + [payload] = response.json() + self.assertEqual(set(payload.keys()), self.SUMMARY_FIELDS) + expected_run = {"status": run.status, "environment": run.environment} if run else None + self.assertEqual(payload["latest_run"], expected_run) + + @parameterized.expand( + [ + ("empty", lambda: []), + ("invalid_uuid", lambda: ["not-a-uuid"]), + ("too_many", lambda: [str(uuid.uuid4()) for _ in range(501)]), + ] + ) + def test_summaries_rejects_invalid_payload(self, _name, ids_factory): + response = self.post_summaries(ids_factory()) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + class TestTaskAutomationAPI(BaseTaskAPITest): @patch("products.tasks.backend.api.sync_automation_schedule") def test_create_automation(self, mock_sync_schedule): From ce013a67e71dfe7c7f53dc070b82635c3425bab2 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Wed, 29 Apr 2026 16:20:04 +0200 Subject: [PATCH 2/8] many=so true --- products/tasks/backend/api.py | 5 ++++- products/tasks/backend/serializers.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/products/tasks/backend/api.py b/products/tasks/backend/api.py index 89a611bff5b0..152e2a5ede03 100644 --- a/products/tasks/backend/api.py +++ b/products/tasks/backend/api.py @@ -217,7 +217,10 @@ def repositories(self, request, **kwargs): @validated_request( request_serializer=TaskSummariesRequestSerializer, responses={ - 200: OpenApiResponse(response=TaskSummarySerializer, description="Summary fields for the requested tasks"), + 200: OpenApiResponse( + response=TaskSummarySerializer(many=True), + description="Summary fields for the requested tasks", + ), }, summary="Fetch task summaries by ID", description=( diff --git a/products/tasks/backend/serializers.py b/products/tasks/backend/serializers.py index db75d5ad18ea..8b097e5dd0e6 100644 --- a/products/tasks/backend/serializers.py +++ b/products/tasks/backend/serializers.py @@ -761,8 +761,8 @@ class TaskSummariesRequestSerializer(serializers.Serializer): class TaskRunSummarySerializer(serializers.Serializer): - status = serializers.CharField(allow_null=True) - environment = serializers.CharField(allow_null=True) + status = serializers.ChoiceField(choices=TaskRun.Status.choices, allow_null=True) + environment = serializers.ChoiceField(choices=TaskRun.Environment.choices, allow_null=True) class TaskSummarySerializer(serializers.ModelSerializer): From 63d6728f4345969a6083abdf499ee9c4e2e2d2a4 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Wed, 29 Apr 2026 17:48:06 +0200 Subject: [PATCH 3/8] better less bad endpoint --- posthog/api/mixins.py | 2 +- products/tasks/backend/api.py | 34 ++++++++++++++++++++---- products/tasks/backend/serializers.py | 7 +++-- products/tasks/backend/tests/test_api.py | 32 +++++++++++++++++++--- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/posthog/api/mixins.py b/posthog/api/mixins.py index 314e2cef37f6..1040fbb6cd65 100644 --- a/posthog/api/mixins.py +++ b/posthog/api/mixins.py @@ -92,7 +92,7 @@ def my_action(self, request: ValidatedRequest, **kwargs): """ def decorator(view_func: Callable) -> Callable: - parameters = [] + parameters: list = list(extend_schema_kwargs.pop("parameters", None) or []) if query_serializer is not None: parameters.append(query_serializer) diff --git a/products/tasks/backend/api.py b/products/tasks/backend/api.py index 152e2a5ede03..11f51eff2487 100644 --- a/products/tasks/backend/api.py +++ b/products/tasks/backend/api.py @@ -18,7 +18,8 @@ import requests as http_requests import jsonschema import posthoganalytics -from drf_spectacular.utils import OpenApiResponse, extend_schema +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework import status, viewsets from rest_framework.authentication import SessionAuthentication from rest_framework.decorators import action @@ -227,22 +228,45 @@ def repositories(self, request, **kwargs): "Returns summary for the requested tasks: `id`, `title`, `repository`, `created_at`, " "`updated_at`, and the latest run's `status` and `environment`." ), + parameters=[ + OpenApiParameter( + name="limit", + type=OpenApiTypes.INT, + location=OpenApiParameter.QUERY, + required=False, + description="Page size for the paginated response.", + ), + OpenApiParameter( + name="offset", + type=OpenApiTypes.INT, + location=OpenApiParameter.QUERY, + required=False, + description="Offset into the result set for pagination.", + ), + ], ) @action( detail=False, methods=["post"], url_path="summaries", required_scopes=["task:read"], - pagination_class=None, filter_backends=[], ) def summaries(self, request, **kwargs): ids = request.validated_data["ids"] latest_run = TaskRun.objects.filter(task=OuterRef("pk")).order_by("-created_at", "-id") - tasks = Task.objects.filter(team=self.team, deleted=False, id__in=ids).annotate( - _latest_run_status=Subquery(latest_run.values("status")[:1]), - _latest_run_environment=Subquery(latest_run.values("environment")[:1]), + tasks = ( + Task.objects.filter(team=self.team, deleted=False, id__in=ids) + .annotate( + _latest_run_status=Subquery(latest_run.values("status")[:1]), + _latest_run_environment=Subquery(latest_run.values("environment")[:1]), + ) + .order_by("-created_at", "id") ) + page = self.paginate_queryset(tasks) + if page is not None: + serializer = TaskSummarySerializer(page, many=True) + return self.get_paginated_response(serializer.data) serializer = TaskSummarySerializer(tasks, many=True) return Response(serializer.data) diff --git a/products/tasks/backend/serializers.py b/products/tasks/backend/serializers.py index 8b097e5dd0e6..8454c85cf920 100644 --- a/products/tasks/backend/serializers.py +++ b/products/tasks/backend/serializers.py @@ -748,7 +748,7 @@ class TaskRunArtifactPresignResponseSerializer(serializers.Serializer): expires_in = serializers.IntegerField(help_text="URL expiry in seconds") -TASK_SUMMARIES_MAX_IDS = 500 +TASK_SUMMARIES_MAX_IDS = 5000 class TaskSummariesRequestSerializer(serializers.Serializer): @@ -756,7 +756,10 @@ class TaskSummariesRequestSerializer(serializers.Serializer): child=serializers.UUIDField(), allow_empty=False, max_length=TASK_SUMMARIES_MAX_IDS, - help_text=f"Task IDs to fetch summaries for (max {TASK_SUMMARIES_MAX_IDS}).", + help_text=( + f"Task IDs to fetch summaries for (max {TASK_SUMMARIES_MAX_IDS}). Response is paginated; " + f"follow the `next` cursor to retrieve all results." + ), ) diff --git a/products/tasks/backend/tests/test_api.py b/products/tasks/backend/tests/test_api.py index a939e9b19ec7..5f174a67b956 100644 --- a/products/tasks/backend/tests/test_api.py +++ b/products/tasks/backend/tests/test_api.py @@ -1705,7 +1705,10 @@ def post_summaries(self, ids): def assertReturnsTaskIds(self, response, expected_ids): self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(sorted(t["id"] for t in response.json()), sorted(str(i) for i in expected_ids)) + self.assertEqual( + sorted(t["id"] for t in response.json()["results"]), + sorted(str(i) for i in expected_ids), + ) def test_summaries_returns_only_requested_ids_scoped_to_team(self): task_a = self.create_task("Task A") @@ -1755,16 +1758,39 @@ def test_summaries_response_shape(self, _name, with_run): response = self.post_summaries([str(task.id)]) self.assertEqual(response.status_code, status.HTTP_200_OK) - [payload] = response.json() + [payload] = response.json()["results"] self.assertEqual(set(payload.keys()), self.SUMMARY_FIELDS) expected_run = {"status": run.status, "environment": run.environment} if run else None self.assertEqual(payload["latest_run"], expected_run) + def test_summaries_paginates_large_id_sets(self): + tasks = [self.create_task(f"Task {i}") for i in range(3)] + ids = [str(t.id) for t in tasks] + + response = self.client.post(f"{self.SUMMARIES_URL}?limit=2", {"ids": ids}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + payload = response.json() + self.assertEqual(payload["count"], 3) + self.assertEqual(len(payload["results"]), 2) + self.assertIsNotNone(payload["next"]) + + # Follow `next` cursor; combined results should equal the full set without duplicates. + next_url = payload["next"] + next_path = next_url[next_url.index("/api/") :] + response2 = self.client.post(next_path, {"ids": ids}, format="json") + self.assertEqual(response2.status_code, status.HTTP_200_OK) + payload2 = response2.json() + self.assertEqual(len(payload2["results"]), 1) + self.assertIsNone(payload2["next"]) + + seen_ids = [r["id"] for r in payload["results"]] + [r["id"] for r in payload2["results"]] + self.assertEqual(sorted(seen_ids), sorted(ids)) + self.assertEqual(len(set(seen_ids)), 3) + @parameterized.expand( [ ("empty", lambda: []), ("invalid_uuid", lambda: ["not-a-uuid"]), - ("too_many", lambda: [str(uuid.uuid4()) for _ in range(501)]), ] ) def test_summaries_rejects_invalid_payload(self, _name, ids_factory): From aa98188c132d3e09a93ac09183f315c7902588e7 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Wed, 29 Apr 2026 18:05:17 +0200 Subject: [PATCH 4/8] comments --- products/tasks/backend/api.py | 12 +++++++----- products/tasks/backend/serializers.py | 5 +---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/products/tasks/backend/api.py b/products/tasks/backend/api.py index 11f51eff2487..f8d3ecd6b69d 100644 --- a/products/tasks/backend/api.py +++ b/products/tasks/backend/api.py @@ -12,6 +12,7 @@ from django.core.cache import cache from django.db import models, transaction from django.db.models import F, OuterRef, Q, Subquery +from django.db.models.functions import JSONObject from django.http import HttpResponse, JsonResponse, StreamingHttpResponse from django.utils import timezone @@ -254,13 +255,14 @@ def repositories(self, request, **kwargs): ) def summaries(self, request, **kwargs): ids = request.validated_data["ids"] - latest_run = TaskRun.objects.filter(task=OuterRef("pk")).order_by("-created_at", "-id") + latest_run = ( + TaskRun.objects.filter(task=OuterRef("pk")) + .order_by("-created_at", "-id") + .annotate(_data=JSONObject(status="status", environment="environment")) + ) tasks = ( Task.objects.filter(team=self.team, deleted=False, id__in=ids) - .annotate( - _latest_run_status=Subquery(latest_run.values("status")[:1]), - _latest_run_environment=Subquery(latest_run.values("environment")[:1]), - ) + .annotate(_latest_run=Subquery(latest_run.values("_data")[:1])) .order_by("-created_at", "id") ) page = self.paginate_queryset(tasks) diff --git a/products/tasks/backend/serializers.py b/products/tasks/backend/serializers.py index 8454c85cf920..9fb75d32cdea 100644 --- a/products/tasks/backend/serializers.py +++ b/products/tasks/backend/serializers.py @@ -778,10 +778,7 @@ class Meta: @extend_schema_field(TaskRunSummarySerializer(allow_null=True)) def get_latest_run(self, obj): - status = getattr(obj, "_latest_run_status", None) - if status is None: - return None - return {"status": status, "environment": getattr(obj, "_latest_run_environment", None)} + return getattr(obj, "_latest_run", None) class TaskListQuerySerializer(serializers.Serializer): From 1fc2cec774b574647b4e8b899bdf329c488551ed Mon Sep 17 00:00:00 2001 From: "tests-posthog[bot]" <250237707+tests-posthog[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:46:49 +0000 Subject: [PATCH 5/8] test(backend): update query snapshots --- posthog/api/test/__snapshots__/test_api_docs.ambr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 9e32d0887ec4..f8ebb6e1be19 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -2,5 +2,7 @@ # name: TestAPIDocsSchema.test_api_docs_generation_warnings_snapshot list([ '', + 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "environment". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "EnvironmentC1cEnum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', + 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "status". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "StatusA35Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', ]) # --- From 4471093cfd8a0db41d0ef6dc8b82482968d3cb39 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 5 May 2026 14:04:45 +0200 Subject: [PATCH 6/8] fix(tasks): add enum overrides for TaskRun status/environment Generated-By: PostHog Code Task-Id: ef5b8a8f-92da-473e-bd2a-e71dd9ffe0d0 --- posthog/api/test/__snapshots__/test_api_docs.ambr | 3 --- posthog/settings/web.py | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index f8ebb6e1be19..3d53ca2c69f1 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -1,8 +1,5 @@ # serializer version: 1 # name: TestAPIDocsSchema.test_api_docs_generation_warnings_snapshot list([ - '', - 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "environment". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "EnvironmentC1cEnum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', - 'Warning: enum naming encountered a non-optimally resolvable collision for fields named "status". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "StatusA35Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.', ]) # --- diff --git a/posthog/settings/web.py b/posthog/settings/web.py index a566a38b5e6b..583024e5d6ff 100644 --- a/posthog/settings/web.py +++ b/posthog/settings/web.py @@ -417,6 +417,8 @@ def static_varies_origin(headers, path, url): "LLMProviderEnum": "products.llm_analytics.backend.models.provider_keys.LLMProvider", "HogFlowStatusEnum": "posthog.models.hog_flow.hog_flow.HogFlow.State", "MCPAuthTypeEnum": "products.mcp_store.backend.models.AUTH_TYPE_CHOICES", + "TaskRunStatusEnum": "products.tasks.backend.models.TaskRun.Status", + "TaskRunEnvironmentEnum": "products.tasks.backend.models.TaskRun.Environment", # --- Inline value lists (type-hint enums, no x-spec-enum-id) --- "PropertyGroupOperator": ["AND", "OR"], "PropertyFilterTypeEnum": [ From e2c21083e1e33a40179f2e178bdc70bba605b2c3 Mon Sep 17 00:00:00 2001 From: "tests-posthog[bot]" <250237707+tests-posthog[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:30:30 +0000 Subject: [PATCH 7/8] chore: update OpenAPI generated types --- .../tasks/frontend/generated/api.schemas.ts | 56 ++++++++++++++++--- products/tasks/frontend/generated/api.ts | 37 ++++++++++++ products/tasks/frontend/generated/api.zod.ts | 15 +++++ services/mcp/src/api/generated.ts | 55 ++++++++++++++++-- 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/products/tasks/frontend/generated/api.schemas.ts b/products/tasks/frontend/generated/api.schemas.ts index f1fbf288eaa0..ea647e22ebb6 100644 --- a/products/tasks/frontend/generated/api.schemas.ts +++ b/products/tasks/frontend/generated/api.schemas.ts @@ -776,9 +776,9 @@ export interface TaskStagedArtifactsPrepareUploadResponseApi { * `failed` - Failed * `cancelled` - Cancelled */ -export type TaskRunDetailStatusEnumApi = (typeof TaskRunDetailStatusEnumApi)[keyof typeof TaskRunDetailStatusEnumApi] +export type TaskRunStatusEnumApi = (typeof TaskRunStatusEnumApi)[keyof typeof TaskRunStatusEnumApi] -export const TaskRunDetailStatusEnumApi = { +export const TaskRunStatusEnumApi = { NotStarted: 'not_started', Queued: 'queued', InProgress: 'in_progress', @@ -791,10 +791,9 @@ export const TaskRunDetailStatusEnumApi = { * * `local` - Local * `cloud` - Cloud */ -export type TaskRunDetailEnvironmentEnumApi = - (typeof TaskRunDetailEnvironmentEnumApi)[keyof typeof TaskRunDetailEnvironmentEnumApi] +export type TaskRunEnvironmentEnumApi = (typeof TaskRunEnvironmentEnumApi)[keyof typeof TaskRunEnvironmentEnumApi] -export const TaskRunDetailEnvironmentEnumApi = { +export const TaskRunEnvironmentEnumApi = { Local: 'local', Cloud: 'cloud', } as const @@ -833,12 +832,12 @@ export interface TaskRunDetailApi { * @nullable */ branch?: string | null - status?: TaskRunDetailStatusEnumApi + status?: TaskRunStatusEnumApi /** Execution environment * `local` - Local * `cloud` - Cloud */ - environment?: TaskRunDetailEnvironmentEnumApi + environment?: TaskRunEnvironmentEnumApi /** Configured runtime adapter for this run, such as 'claude' or 'codex'. */ readonly runtime_adapter: RuntimeAdapterEnumApi | NullEnumApi | null /** Configured LLM provider for this run, such as 'anthropic' or 'openai'. */ @@ -1429,6 +1428,38 @@ export interface RepositoryReadinessResponseApi { scan?: ScanEvidenceApi } +export interface TaskSummariesRequestApi { + /** + * Task IDs to fetch summaries for (max 5000). Response is paginated; follow the `next` cursor to retrieve all results. + * @maxItems 5000 + */ + ids: string[] +} + +export interface TaskRunSummaryApi { + status: TaskRunStatusEnumApi | NullEnumApi | null + environment: TaskRunEnvironmentEnumApi | NullEnumApi | null +} + +export interface TaskSummaryApi { + readonly id: string + readonly title: string + /** @nullable */ + readonly repository: string | null + readonly created_at: string + readonly updated_at: string + readonly latest_run: TaskRunSummaryApi | null +} + +export interface PaginatedTaskSummaryListApi { + count: number + /** @nullable */ + next?: string | null + /** @nullable */ + previous?: string | null + results: TaskSummaryApi[] +} + export type SandboxListParams = { /** * Number of results to return per page. @@ -1569,3 +1600,14 @@ export type TasksRepositoryReadinessRetrieveParams = { */ window_days?: number } + +export type TasksSummariesCreateParams = { + /** + * Page size for the paginated response. + */ + limit?: number + /** + * Offset into the result set for pagination. + */ + offset?: number +} diff --git a/products/tasks/frontend/generated/api.ts b/products/tasks/frontend/generated/api.ts index 1f5444c72d13..183032add74c 100644 --- a/products/tasks/frontend/generated/api.ts +++ b/products/tasks/frontend/generated/api.ts @@ -15,6 +15,7 @@ import type { PaginatedTaskAutomationListApi, PaginatedTaskListApi, PaginatedTaskRunDetailListApi, + PaginatedTaskSummaryListApi, PatchedTaskApi, PatchedTaskRunSetOutputRequestApi, PatchedTaskRunUpdateApi, @@ -46,10 +47,12 @@ import type { TaskStagedArtifactsFinalizeUploadResponseApi, TaskStagedArtifactsPrepareUploadRequestApi, TaskStagedArtifactsPrepareUploadResponseApi, + TaskSummariesRequestApi, TasksListParams, TasksRepositoryReadinessRetrieveParams, TasksRunsListParams, TasksRunsSessionLogsRetrieveParams, + TasksSummariesCreateParams, } from './api.schemas' // https://stackoverflow.com/questions/49579094/typescript-conditional-types-filter-out-readonly-properties-pick-only-requir/49579497#49579497 @@ -871,3 +874,37 @@ export const tasksRepositoryReadinessRetrieve = async ( method: 'GET', }) } + +/** + * Returns summary for the requested tasks: `id`, `title`, `repository`, `created_at`, `updated_at`, and the latest run's `status` and `environment`. + * @summary Fetch task summaries by ID + */ +export const getTasksSummariesCreateUrl = (projectId: string, params?: TasksSummariesCreateParams) => { + const normalizedParams = new URLSearchParams() + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }) + + const stringifiedParams = normalizedParams.toString() + + return stringifiedParams.length > 0 + ? `/api/projects/${projectId}/tasks/summaries/?${stringifiedParams}` + : `/api/projects/${projectId}/tasks/summaries/` +} + +export const tasksSummariesCreate = async ( + projectId: string, + taskSummariesRequestApi: TaskSummariesRequestApi, + params?: TasksSummariesCreateParams, + options?: RequestInit +): Promise => { + return apiMutator(getTasksSummariesCreateUrl(projectId, params), { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(taskSummariesRequestApi), + }) +} diff --git a/products/tasks/frontend/generated/api.zod.ts b/products/tasks/frontend/generated/api.zod.ts index b4f99fbd1af2..fd1cc21a892f 100644 --- a/products/tasks/frontend/generated/api.zod.ts +++ b/products/tasks/frontend/generated/api.zod.ts @@ -904,3 +904,18 @@ export const TasksRunsStartCreateBody = /* @__PURE__ */ zod.object({ 'Identifiers for run artifacts that should be attached to the next user message delivered to the sandbox.' ), }) + +/** + * Returns summary for the requested tasks: `id`, `title`, `repository`, `created_at`, `updated_at`, and the latest run's `status` and `environment`. + * @summary Fetch task summaries by ID + */ +export const tasksSummariesCreateBodyIdsMax = 5000 + +export const TasksSummariesCreateBody = /* @__PURE__ */ zod.object({ + ids: zod + .array(zod.uuid()) + .max(tasksSummariesCreateBodyIdsMax) + .describe( + 'Task IDs to fetch summaries for (max 5000). Response is paginated; follow the `next` cursor to retrieve all results.' + ), +}) diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index 5de1a6919c71..e5c634b02f36 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -25409,10 +25409,10 @@ export namespace Schemas { * `failed` - Failed * `cancelled` - Cancelled */ - export type TaskRunDetailStatusEnum = typeof TaskRunDetailStatusEnum[keyof typeof TaskRunDetailStatusEnum]; + export type TaskRunStatusEnum = typeof TaskRunStatusEnum[keyof typeof TaskRunStatusEnum]; - export const TaskRunDetailStatusEnum = { + export const TaskRunStatusEnum = { NotStarted: 'not_started', Queued: 'queued', InProgress: 'in_progress', @@ -25425,10 +25425,10 @@ export namespace Schemas { * * `local` - Local * `cloud` - Cloud */ - export type TaskRunDetailEnvironmentEnum = typeof TaskRunDetailEnvironmentEnum[keyof typeof TaskRunDetailEnvironmentEnum]; + export type TaskRunEnvironmentEnum = typeof TaskRunEnvironmentEnum[keyof typeof TaskRunEnvironmentEnum]; - export const TaskRunDetailEnvironmentEnum = { + export const TaskRunEnvironmentEnum = { Local: 'local', Cloud: 'cloud', } as const; @@ -25487,12 +25487,12 @@ export namespace Schemas { * @nullable */ branch?: string | null; - status?: TaskRunDetailStatusEnum; + status?: TaskRunStatusEnum; /** Execution environment * `local` - Local * `cloud` - Cloud */ - environment?: TaskRunDetailEnvironmentEnum; + environment?: TaskRunEnvironmentEnum; /** Configured runtime adapter for this run, such as 'claude' or 'codex'. */ readonly runtime_adapter: RuntimeAdapterEnum | NullEnum | null; /** Configured LLM provider for this run, such as 'anthropic' or 'openai'. */ @@ -25534,6 +25534,30 @@ export namespace Schemas { results: TaskRunDetail[]; } + export interface TaskRunSummary { + status: TaskRunStatusEnum | NullEnum | null; + environment: TaskRunEnvironmentEnum | NullEnum | null; + } + + export interface TaskSummary { + readonly id: string; + readonly title: string; + /** @nullable */ + readonly repository: string | null; + readonly created_at: string; + readonly updated_at: string; + readonly latest_run: TaskRunSummary | null; + } + + export interface PaginatedTaskSummaryList { + count: number; + /** @nullable */ + next?: string | null; + /** @nullable */ + previous?: string | null; + results: TaskSummary[]; + } + /** * Serializer for `Team` model with minimal attributes to speeed up loading and transfer times. Also used for nested serializers. @@ -37125,6 +37149,14 @@ export namespace Schemas { artifacts: TaskStagedArtifactPrepareUploadResponse[]; } + export interface TaskSummariesRequest { + /** + * Task IDs to fetch summaries for (max 5000). Response is paginated; follow the `next` cursor to retrieve all results. + * @maxItems 5000 + */ + ids: string[]; + } + export type TeamDefaultModifiers = { [key: string]: unknown }; export type TeamGroupTypesItem = { [key: string]: unknown }; @@ -45305,6 +45337,17 @@ export namespace Schemas { window_days?: number; }; + export type TasksSummariesCreateParams = { + /** + * Page size for the paginated response. + */ + limit?: number; + /** + * Offset into the result set for pagination. + */ + offset?: number; + }; + export type UploadedMediaCreate201 = { [key: string]: unknown }; export type UserProductListListParams = { From 47b83e4117520e780994bd2b88e698d6d254acb5 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 5 May 2026 16:27:03 +0200 Subject: [PATCH 8/8] fix(tasks): restore empty-string entry in api docs warnings snapshot Generated-By: PostHog Code Task-Id: 77ffbb16-3a1a-482a-9af0-8c8c563fb8d8 --- posthog/api/test/__snapshots__/test_api_docs.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 3d53ca2c69f1..9e32d0887ec4 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -1,5 +1,6 @@ # serializer version: 1 # name: TestAPIDocsSchema.test_api_docs_generation_warnings_snapshot list([ + '', ]) # ---