mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(marketing): release and spotlight drafts carry their source project
draft_release_post / draft_release_video accept a project_id and the release-proposal approve hooks pass the proposal task's own project; the spotlight companion video forwards the spotlight draft's project into open_video_task — previously it always authored against the deployment-anchor project's motion/ tree regardless of which project the spotlight was about. Omitted project_id keeps the anchor-project fallback, so single-project deployments are unchanged.
This commit is contained in:
@@ -13,7 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import redis.asyncio as redis
|
||||
@@ -263,8 +263,9 @@ class ReleaseProposalService(BaseService):
|
||||
if result.status in ("published", "already_published"):
|
||||
task.status = TaskStatus.COMPLETED
|
||||
await self.session.flush()
|
||||
await self._draft_x_post(report)
|
||||
await self._draft_video(report)
|
||||
release_project_id = cast("UUID | None", task.project_id)
|
||||
await self._draft_x_post(report, release_project_id)
|
||||
await self._draft_video(report, release_project_id)
|
||||
await self._draft_docs_update(report)
|
||||
return result
|
||||
finally:
|
||||
@@ -273,32 +274,40 @@ class ReleaseProposalService(BaseService):
|
||||
)
|
||||
await self._close_redis()
|
||||
|
||||
async def _draft_x_post(self, report: ReleaseReadinessReport) -> None:
|
||||
async def _draft_x_post(
|
||||
self, report: ReleaseReadinessReport, project_id: UUID | None
|
||||
) -> None:
|
||||
"""Hand the just-published release to the X engine for a held
|
||||
announcement draft (best-effort — never raises into approve(); a
|
||||
drafting failure must not affect the release's already-succeeded
|
||||
publish). Off/no-creds is itself a no-op inside the engine."""
|
||||
publish). Off/no-creds is itself a no-op inside the engine. The
|
||||
proposal task's project scopes the draft to the released project."""
|
||||
try:
|
||||
from roboco.services.x_engine import get_x_engine
|
||||
|
||||
await get_x_engine(self.session).draft_release_post(
|
||||
version=report.proposed_version,
|
||||
highlights=list(report.change_summary),
|
||||
project_id=project_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("x-post draft failed (best-effort): %s", exc)
|
||||
|
||||
async def _draft_video(self, report: ReleaseReadinessReport) -> None:
|
||||
async def _draft_video(
|
||||
self, report: ReleaseReadinessReport, project_id: UUID | None
|
||||
) -> None:
|
||||
"""Hand the just-published release to the video engine for a held
|
||||
UX/UI authoring task (best-effort — never raises into approve(); a
|
||||
drafting failure must not affect the release's already-succeeded
|
||||
publish). Off/no-sub-switch is itself a no-op inside the engine."""
|
||||
publish). Off/no-sub-switch is itself a no-op inside the engine. The
|
||||
proposal task's project scopes the draft to the released project."""
|
||||
try:
|
||||
from roboco.services.video_engine import get_video_engine
|
||||
|
||||
await get_video_engine(self.session).draft_release_video(
|
||||
version=report.proposed_version,
|
||||
changelog=report.drafted_changelog,
|
||||
project_id=project_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("video draft failed (best-effort): %s", exc)
|
||||
|
||||
@@ -437,7 +437,7 @@ class VideoEngine(BaseService):
|
||||
# ---- release trigger (event-driven hook) -------------------------------
|
||||
|
||||
async def draft_release_video(
|
||||
self, *, version: str, changelog: str
|
||||
self, *, version: str, changelog: str, project_id: UUID | None = None
|
||||
) -> TaskTable | None:
|
||||
"""Originate ONE UX/UI video-authoring task for a release announcement,
|
||||
or None (no-op).
|
||||
@@ -464,6 +464,7 @@ class VideoEngine(BaseService):
|
||||
platforms=["x", "tiktok"],
|
||||
brief=brief,
|
||||
suggested_input_props={"version": version, "highlights": highlights},
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
async def _draft_release_script(self, version: str, changelog: str) -> str:
|
||||
|
||||
@@ -243,6 +243,12 @@ class XEngine(BaseService):
|
||||
slug = (settings.self_heal_project_slug or "roboco-api").strip()
|
||||
return await get_project_service(self.session).get_by_slug(slug)
|
||||
|
||||
async def _project_or_default(self, project_id: UUID | None) -> ProjectTable | None:
|
||||
"""The explicitly-targeted project, or the deployment-anchor fallback."""
|
||||
if project_id is not None:
|
||||
return await get_project_service(self.session).get(project_id)
|
||||
return await self._roboco_project()
|
||||
|
||||
async def _voice_guide(self) -> str:
|
||||
"""Baseline house style plus the CEO's brand-voice sample, when set.
|
||||
|
||||
@@ -263,14 +269,16 @@ class XEngine(BaseService):
|
||||
# ---- release posts (event-driven hook) --------------------------------
|
||||
|
||||
async def draft_release_post(
|
||||
self, *, version: str, highlights: list[str]
|
||||
self, *, version: str, highlights: list[str], project_id: UUID | None = None
|
||||
) -> TaskTable | None:
|
||||
"""Originate ONE held release-announcement draft, or None (no-op).
|
||||
|
||||
No-ops when the flag is off, no credentials are configured, a draft
|
||||
for this version already exists (retry-safe), or the open-post cap is
|
||||
reached. Called from ``ReleaseProposalService.approve()``'s publish
|
||||
success branch — never invoked by the loop itself.
|
||||
success branch — never invoked by the loop itself. ``project_id``
|
||||
scopes the draft to the released project; omitted falls back to the
|
||||
deployment-anchor project.
|
||||
"""
|
||||
if not settings.x_engine_enabled:
|
||||
return None
|
||||
@@ -291,10 +299,10 @@ class XEngine(BaseService):
|
||||
version=version,
|
||||
)
|
||||
return None
|
||||
project = await self._roboco_project()
|
||||
project = await self._project_or_default(project_id)
|
||||
if project is None or project.id is None:
|
||||
self.log.warning(
|
||||
"x-engine: RoboCo project not resolvable; skipping release post",
|
||||
"x-engine: target project not resolvable; skipping release post",
|
||||
version=version,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import redis.asyncio as redis
|
||||
@@ -244,6 +244,10 @@ class XPostService(BaseService):
|
||||
script=video_script.strip() or feature_brief,
|
||||
platforms=["x", "tiktok"],
|
||||
brief=feature_brief,
|
||||
# The spotlight draft's own project — without it the video
|
||||
# authors against the deployment-anchor project's motion/
|
||||
# tree regardless of which project the spotlight is about.
|
||||
project_id=cast("UUID | None", task.project_id),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("spotlight video draft failed (best-effort): %s", exc)
|
||||
|
||||
@@ -147,7 +147,7 @@ async def test_publish_success_calls_video_engine_draft_seam(
|
||||
assert result is not None
|
||||
assert result.status == "published"
|
||||
fake_video_engine.draft_release_video.assert_awaited_once_with(
|
||||
version=_VERSION, changelog=_CHANGELOG
|
||||
version=_VERSION, changelog=_CHANGELOG, project_id=task.project_id
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -139,7 +139,9 @@ async def test_publish_success_calls_x_engine_draft_seam(
|
||||
assert result is not None
|
||||
assert result.status == "published"
|
||||
fake_engine.draft_release_post.assert_awaited_once_with(
|
||||
version=_VERSION, highlights=["feat: a thing", "fix: another thing"]
|
||||
version=_VERSION,
|
||||
highlights=["feat: a thing", "fix: another thing"],
|
||||
project_id=task.project_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -584,6 +584,9 @@ async def test_approve_feature_spotlight_with_video_opens_video_task(
|
||||
assert kwargs["platforms"] == ["x", "tiktok"]
|
||||
assert kwargs["script"] == "Custom voiceover script"
|
||||
assert kwargs["brief"] == "Organizational Memory Loop: Draft body"
|
||||
# The spotlight's own project scopes the video authoring — without it the
|
||||
# video authored against the deployment-anchor project regardless.
|
||||
assert kwargs["project_id"] == task.project_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user