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 asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
@@ -263,8 +263,9 @@ class ReleaseProposalService(BaseService):
|
|||||||
if result.status in ("published", "already_published"):
|
if result.status in ("published", "already_published"):
|
||||||
task.status = TaskStatus.COMPLETED
|
task.status = TaskStatus.COMPLETED
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
await self._draft_x_post(report)
|
release_project_id = cast("UUID | None", task.project_id)
|
||||||
await self._draft_video(report)
|
await self._draft_x_post(report, release_project_id)
|
||||||
|
await self._draft_video(report, release_project_id)
|
||||||
await self._draft_docs_update(report)
|
await self._draft_docs_update(report)
|
||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
@@ -273,32 +274,40 @@ class ReleaseProposalService(BaseService):
|
|||||||
)
|
)
|
||||||
await self._close_redis()
|
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
|
"""Hand the just-published release to the X engine for a held
|
||||||
announcement draft (best-effort — never raises into approve(); a
|
announcement draft (best-effort — never raises into approve(); a
|
||||||
drafting failure must not affect the release's already-succeeded
|
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:
|
try:
|
||||||
from roboco.services.x_engine import get_x_engine
|
from roboco.services.x_engine import get_x_engine
|
||||||
|
|
||||||
await get_x_engine(self.session).draft_release_post(
|
await get_x_engine(self.session).draft_release_post(
|
||||||
version=report.proposed_version,
|
version=report.proposed_version,
|
||||||
highlights=list(report.change_summary),
|
highlights=list(report.change_summary),
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("x-post draft failed (best-effort): %s", 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
|
"""Hand the just-published release to the video engine for a held
|
||||||
UX/UI authoring task (best-effort — never raises into approve(); a
|
UX/UI authoring task (best-effort — never raises into approve(); a
|
||||||
drafting failure must not affect the release's already-succeeded
|
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:
|
try:
|
||||||
from roboco.services.video_engine import get_video_engine
|
from roboco.services.video_engine import get_video_engine
|
||||||
|
|
||||||
await get_video_engine(self.session).draft_release_video(
|
await get_video_engine(self.session).draft_release_video(
|
||||||
version=report.proposed_version,
|
version=report.proposed_version,
|
||||||
changelog=report.drafted_changelog,
|
changelog=report.drafted_changelog,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("video draft failed (best-effort): %s", exc)
|
logger.warning("video draft failed (best-effort): %s", exc)
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ class VideoEngine(BaseService):
|
|||||||
# ---- release trigger (event-driven hook) -------------------------------
|
# ---- release trigger (event-driven hook) -------------------------------
|
||||||
|
|
||||||
async def draft_release_video(
|
async def draft_release_video(
|
||||||
self, *, version: str, changelog: str
|
self, *, version: str, changelog: str, project_id: UUID | None = None
|
||||||
) -> TaskTable | None:
|
) -> TaskTable | None:
|
||||||
"""Originate ONE UX/UI video-authoring task for a release announcement,
|
"""Originate ONE UX/UI video-authoring task for a release announcement,
|
||||||
or None (no-op).
|
or None (no-op).
|
||||||
@@ -464,6 +464,7 @@ class VideoEngine(BaseService):
|
|||||||
platforms=["x", "tiktok"],
|
platforms=["x", "tiktok"],
|
||||||
brief=brief,
|
brief=brief,
|
||||||
suggested_input_props={"version": version, "highlights": highlights},
|
suggested_input_props={"version": version, "highlights": highlights},
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _draft_release_script(self, version: str, changelog: str) -> str:
|
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()
|
slug = (settings.self_heal_project_slug or "roboco-api").strip()
|
||||||
return await get_project_service(self.session).get_by_slug(slug)
|
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:
|
async def _voice_guide(self) -> str:
|
||||||
"""Baseline house style plus the CEO's brand-voice sample, when set.
|
"""Baseline house style plus the CEO's brand-voice sample, when set.
|
||||||
|
|
||||||
@@ -263,14 +269,16 @@ class XEngine(BaseService):
|
|||||||
# ---- release posts (event-driven hook) --------------------------------
|
# ---- release posts (event-driven hook) --------------------------------
|
||||||
|
|
||||||
async def draft_release_post(
|
async def draft_release_post(
|
||||||
self, *, version: str, highlights: list[str]
|
self, *, version: str, highlights: list[str], project_id: UUID | None = None
|
||||||
) -> TaskTable | None:
|
) -> TaskTable | None:
|
||||||
"""Originate ONE held release-announcement draft, or None (no-op).
|
"""Originate ONE held release-announcement draft, or None (no-op).
|
||||||
|
|
||||||
No-ops when the flag is off, no credentials are configured, a draft
|
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
|
for this version already exists (retry-safe), or the open-post cap is
|
||||||
reached. Called from ``ReleaseProposalService.approve()``'s publish
|
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:
|
if not settings.x_engine_enabled:
|
||||||
return None
|
return None
|
||||||
@@ -291,10 +299,10 @@ class XEngine(BaseService):
|
|||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
project = await self._roboco_project()
|
project = await self._project_or_default(project_id)
|
||||||
if project is None or project.id is None:
|
if project is None or project.id is None:
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"x-engine: RoboCo project not resolvable; skipping release post",
|
"x-engine: target project not resolvable; skipping release post",
|
||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import redis.asyncio as redis
|
import redis.asyncio as redis
|
||||||
@@ -244,6 +244,10 @@ class XPostService(BaseService):
|
|||||||
script=video_script.strip() or feature_brief,
|
script=video_script.strip() or feature_brief,
|
||||||
platforms=["x", "tiktok"],
|
platforms=["x", "tiktok"],
|
||||||
brief=feature_brief,
|
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:
|
except Exception as exc:
|
||||||
logger.warning("spotlight video draft failed (best-effort): %s", 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 is not None
|
||||||
assert result.status == "published"
|
assert result.status == "published"
|
||||||
fake_video_engine.draft_release_video.assert_awaited_once_with(
|
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 is not None
|
||||||
assert result.status == "published"
|
assert result.status == "published"
|
||||||
fake_engine.draft_release_post.assert_awaited_once_with(
|
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["platforms"] == ["x", "tiktok"]
|
||||||
assert kwargs["script"] == "Custom voiceover script"
|
assert kwargs["script"] == "Custom voiceover script"
|
||||||
assert kwargs["brief"] == "Organizational Memory Loop: Draft body"
|
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
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user