fix release rollback guards and sync api docs

This commit is contained in:
Chris
2026-03-23 22:56:19 +08:00
parent 752d7aba4d
commit 9a43563d45
5 changed files with 73 additions and 41 deletions

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import asdict
from typing import Any
from fastapi import HTTPException
from app.domain.editor import VariantChange
from app.domain.editor_builder import build_runtime_payload_from_changes
from app.domain.mappers import to_release
@@ -25,6 +27,24 @@ class ReleaseService:
self.variant_repository = variant_repository or VariantRepository()
self.change_repository = change_repository or VariantChangeRepository()
async def _enforce_single_published(
self,
experiment_id: str,
keep_release_id: str,
access_token: str | None = None,
) -> None:
published_items = await self.repository.list(
params={
"filter[experiment_id][_eq]": experiment_id,
"filter[status][_eq]": "published",
},
access_token=access_token,
)
for item in published_items:
item_id = str(item["id"])
if item_id != keep_release_id:
await self.repository.update(item_id, {"status": "draft"}, access_token=access_token)
async def list_releases(
self,
experiment_id: str | None = None,
@@ -118,21 +138,14 @@ class ReleaseService:
experiment_id = str(target["experiment_id"])
# Demote any currently published release in this experiment to draft
currently_published = await self.repository.list(
params={
"filter[experiment_id][_eq]": experiment_id,
"filter[status][_eq]": "published",
},
access_token=access_token,
)
for other in currently_published:
if str(other["id"]) != release_id:
await self.repository.update(str(other["id"]), {"status": "draft"}, access_token=access_token)
item = await self.repository.update(release_id, {"status": "published"}, access_token=access_token)
if not item:
return None
await self._enforce_single_published(
experiment_id=experiment_id,
keep_release_id=release_id,
access_token=access_token,
)
return ReleaseLifecycleResponse(
id=str(item["id"]),
status=str(item["status"]),
@@ -150,44 +163,56 @@ class ReleaseService:
return None
experiment_id = str(target["experiment_id"])
target_status = str(target.get("status", ""))
if target_status != "published":
raise HTTPException(
status_code=409,
detail=f"Release '{release_id}' is not published; rollback is only allowed for published releases.",
)
current_version_no = int(target["version_no"])
# Demote the current release back to draft
await self.repository.update(release_id, {"status": "draft"}, access_token=access_token)
# Find the most recent draft release with an earlier version_no to restore
# Find the most recent non-archived release with an earlier version_no to restore.
previous_candidates = await self.repository.list(
params={
"filter[experiment_id][_eq]": experiment_id,
"filter[status][_eq]": "draft",
"filter[version_no][_lt]": str(current_version_no),
"filter[status][_neq]": "archived",
"sort": "-version_no",
"limit": "1",
},
access_token=access_token,
)
if previous_candidates:
restored = await self.repository.update(
str(previous_candidates[0]["id"]),
{"status": "published"},
access_token=access_token,
if not previous_candidates:
raise HTTPException(
status_code=409,
detail=f"Release '{release_id}' cannot rollback because no earlier non-archived release exists.",
)
if restored:
return ReleaseLifecycleResponse(
id=str(restored["id"]),
status=str(restored["status"]),
version_no=int(restored["version_no"]),
)
# No previous release to restore — return the now-drafted current release
updated = await self.repository.get(release_id, access_token=access_token)
if not updated:
return None
# Demote current published release, then promote previous release.
await self.repository.update(release_id, {"status": "draft"}, access_token=access_token)
restored_id = str(previous_candidates[0]["id"])
restored = await self.repository.update(
restored_id,
{"status": "published"},
access_token=access_token,
)
if not restored:
raise HTTPException(
status_code=500,
detail="Rollback failed while promoting the target previous release.",
)
await self._enforce_single_published(
experiment_id=experiment_id,
keep_release_id=restored_id,
access_token=access_token,
)
return ReleaseLifecycleResponse(
id=str(updated["id"]),
status=str(updated["status"]),
version_no=int(updated["version_no"]),
id=str(restored["id"]),
status=str(restored["status"]),
version_no=int(restored["version_no"]),
)
async def archive_release(