Finish STATE-WP-0069: retire legacy completion event and DELETE /workstreams
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Stop dual-publishing org.statehub.workstream.completed; return 410 Gone for
legacy DELETE /workstreams/{id}. Migrate fix-consistency, MCP adhoc bootstrap,
and dashboard token summary to /workplans/. Add legacy-meter evidence capture
script and pytest snapshot; update docs and close out the workplan.
This commit is contained in:
tegwick 2026-07-08 23:36:58 +02:00
parent b659ff8d13
commit e0c954d098
13 changed files with 404 additions and 149 deletions

View file

@ -23,12 +23,8 @@ from api.schemas.workplan import (
WorkplanUpdate,
)
from api.services.lifecycle import transition_workplan_status
from api.services.legacy_compat import mark_legacy_response
from api.services.legacy_meter import (
LegacyUsageIdentity,
identity_from_request,
record_legacy_usage,
)
from api.services.legacy_compat import legacy_response_headers, mark_legacy_response
from api.services.legacy_meter import identity_from_request, record_legacy_usage
from api.workplan_status import (
is_supported_workplan_status,
normalize_workplan_status,
@ -47,7 +43,6 @@ _INDEX_REFRESH_TASK: asyncio.Task | None = None
_INDEX_LAST_ERROR: str | None = None
_LEGACY_OWNER = "state-hub.api"
_COMPLETED_WORKSTREAM_EVENT = "org.statehub.workstream.completed"
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"
@ -111,27 +106,6 @@ async def _meter_legacy_route(
logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)
async def _meter_legacy_event(
*,
session: AsyncSession,
subject: str,
replacement_ref: str,
) -> None:
try:
await record_legacy_usage(
session,
interface_key=f"event_subject:{subject}",
interface_kind="event_subject",
replacement_ref=replacement_ref,
owner_component="state-hub.events",
replacement_verified=True,
identity=LegacyUsageIdentity(component_key="state-hub.events"),
)
except Exception:
await session.rollback()
logger.warning("legacy-meter failed to record event subject %s", subject, exc_info=True)
async def _list_workplans(
*,
topic_id: uuid.UUID | None,
@ -377,7 +351,6 @@ async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> Non
_COMPLETED_WORKPLAN_EVENT,
attributes={
"workplan_id": str(wp.id),
"legacy_workstream_id": str(wp.id),
"slug": wp.slug,
"title": wp.title,
"topic_id": str(wp.topic_id) if wp.topic_id else None,
@ -387,24 +360,6 @@ async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> Non
)
asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))
await _meter_legacy_event(
session=session,
subject=_COMPLETED_WORKSTREAM_EVENT,
replacement_ref=_COMPLETED_WORKPLAN_EVENT,
)
legacy_envelope = EventEnvelope.new(
_COMPLETED_WORKSTREAM_EVENT,
attributes={
"workstream_id": str(wp.id),
"slug": wp.slug,
"title": wp.title,
"topic_id": str(wp.topic_id) if wp.topic_id else None,
"repo_id": str(wp.repo_id) if wp.repo_id else None,
"repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
},
)
asyncio.create_task(publish_event(_COMPLETED_WORKSTREAM_EVENT, legacy_envelope))
@router.get("/", response_model=list[WorkplanRead])
async def list_workstreams(
@ -581,21 +536,26 @@ async def update_workplan(
return await _update_workplan(workplan_id=workplan_id, body=body, session=session)
@router.delete("/{workstream_id}", response_model=WorkplanRead)
@router.delete("/{workstream_id}", status_code=status.HTTP_410_GONE)
async def archive_workstream(
request: Request,
response: Response,
workstream_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
) -> Workplan:
) -> None:
replacement_ref = "DELETE /workplans/{workplan_id}"
await _meter_legacy_route(
session=session,
request=request,
response=response,
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
replacement_ref="/workplans/{workplan_id}",
replacement_ref=replacement_ref,
)
raise HTTPException(
status_code=status.HTTP_410_GONE,
detail="Legacy DELETE /workstreams/{workstream_id} retired; use DELETE /workplans/{workplan_id}",
headers=legacy_response_headers(replacement_ref),
)
return await _archive_workplan(workplan_id=workstream_id, session=session)
@workplan_router.delete("/{workplan_id}", response_model=WorkplanRead)

View file

@ -16,13 +16,23 @@ _LEGACY_OWNER = "state-hub.api"
_LEGACY_WORKSTREAM_SUNSET = "Wed, 30 Jun 2027 23:59:59 GMT"
def legacy_response_headers(replacement_ref: str) -> dict[str, str]:
return {
"Deprecation": "true",
"Sunset": _LEGACY_WORKSTREAM_SUNSET,
"X-StateHub-Replacement": replacement_ref,
"Link": f'<{replacement_ref}>; rel="successor-version"',
}
def mark_legacy_response(response: Response | None, replacement_ref: str) -> None:
if response is None:
return
response.headers["Deprecation"] = "true"
response.headers["Sunset"] = _LEGACY_WORKSTREAM_SUNSET
response.headers["X-StateHub-Replacement"] = replacement_ref
response.headers.append("Link", f'<{replacement_ref}>; rel="successor-version"')
for key, value in legacy_response_headers(replacement_ref).items():
if key == "Link":
response.headers.append(key, value)
else:
response.headers[key] = value
def legacy_query_param_key(method: str, route: str, param: str = "workstream_id") -> str: