From 61de736a5df364afdbfc1e2ddbd598d2131fdba6 Mon Sep 17 00:00:00 2001 From: eabdullin Date: Tue, 1 Sep 2026 16:08:34 +0300 Subject: [PATCH] Expose every release a build has been put into builds.release_id only keeps the release a build got into last, so a build released more than once, to different products or platforms, has no way to tell about the earlier releases. The build to releases link lives on the other side, in build_releases.build_ids, and there is no way to query it: GET /releases/ takes no build_id, and every release it returns carries the whole release plan, megabytes of it. Add GET /builds/{build_id}/releases/, which looks the history up from the releases side with :build_id = ANY(build_ids). Columns are selected explicitly so the plan stays out of both the query and the response. Reverted releases are returned along with the rest, each with its status, and it is up to the caller to decide what to show. --- alws/crud/build.py | 33 +++++++++++++++++++++++++++++++++ alws/routers/builds.py | 11 +++++++++++ alws/schemas/build_schema.py | 15 +++++++++++++++ tests/test_api/test_releases.py | 22 ++++++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/alws/crud/build.py b/alws/crud/build.py index eea70886..548127c7 100644 --- a/alws/crud/build.py +++ b/alws/crud/build.py @@ -239,6 +239,39 @@ async def generate_query(count=False): return query.scalars().all() +async def get_build_releases( + db: AsyncSession, + build_id: int, +) -> typing.List[build_schema.BuildRelease]: + """ + Every release the build has ever been put into, newest first. + + models.Build.release_id only keeps the release the build got into last, + so the full history has to be looked up from the releases side. Columns + are selected explicitly to keep the huge release plan out of the query. + """ + result = await db.execute( + select( + models.Release.id, + models.Release.status, + models.Release.created_at, + models.Platform.name.label("platform_name"), + models.Product.name.label("product_name"), + ) + .join( + models.Platform, + models.Release.platform_id == models.Platform.id, + ) + .join( + models.Product, + models.Release.product_id == models.Product.id, + ) + .where(models.Release.build_ids.any(build_id)) + .order_by(models.Release.id.desc()) + ) + return [build_schema.BuildRelease(**row._asdict()) for row in result.all()] + + async def get_module_preview( redis: aioredis.client.Redis, platform: models.Platform, diff --git a/alws/routers/builds.py b/alws/routers/builds.py index 08fcd860..9da0f2df 100644 --- a/alws/routers/builds.py +++ b/alws/routers/builds.py @@ -117,6 +117,17 @@ async def get_build( return db_build +@public_router.get( + '/{build_id}/releases/', + response_model=typing.List[build_schema.BuildRelease], +) +async def get_build_releases( + build_id: int, + db: AsyncSession = Depends(AsyncSessionDependency(key=get_async_db_key())), +): + return await build_crud.get_build_releases(db, build_id) + + @router.patch('/{build_id}/restart-failed', status_code=status.HTTP_200_OK) async def restart_failed_build_items( build_id: int, diff --git a/alws/schemas/build_schema.py b/alws/schemas/build_schema.py index d4e22b40..9c3118e0 100644 --- a/alws/schemas/build_schema.py +++ b/alws/schemas/build_schema.py @@ -248,6 +248,21 @@ class Config: from_attributes = True +class BuildRelease(BaseModel): + """ + A release the build is a part of, without the heavy release plan. + """ + + id: int + status: int + created_at: typing.Optional[datetime.datetime] = None + platform_name: typing.Optional[str] = None + product_name: typing.Optional[str] = None + + class Config: + from_attributes = True + + class Build(BaseModel): id: int created_at: datetime.datetime diff --git a/tests/test_api/test_releases.py b/tests/test_api/test_releases.py index 185351a0..9db7640b 100644 --- a/tests/test_api/test_releases.py +++ b/tests/test_api/test_releases.py @@ -158,6 +158,28 @@ async def test_get_release( message = f"Cannot retrieve release:\n{response.text}" assert response.status_code == self.status_codes.HTTP_200_OK, message + async def test_get_build_releases( + self, + ): + self.headers = {} + response = await self.make_request( + "get", + "/api/v1/releases/", + ) + release = response.json()[0] + build_id = release["build_ids"][0] + response = await self.make_request( + "get", + f"/api/v1/builds/{build_id}/releases/", + ) + message = f"Cannot retrieve build releases:\n{response.text}" + assert response.status_code == self.status_codes.HTTP_200_OK, message + message = ( + f"Release {release['id']} is missing " + f"among the releases of build {build_id}" + ) + assert release["id"] in [row["id"] for row in response.json()], message + async def test_revert_release( self, async_session: AsyncSession,