fix(x): release caption uses curated CHANGELOG headlines, not commit subjects (#607)

draft_release_post was fed highlights=list(report.change_summary) — raw
per-commit subjects — so the announcement model parroted the top commit
('RoboCo API v0.26.0 is out: docs: curate the full Unreleased body #601').
New pure changelog_highlights() extracts the bold feature leads from the
curated release entry (report.drafted_changelog), stripping PR refs and
trailing periods; approve() prefers those and falls back to change_summary
only when the changelog yields nothing. The video captions were already
good because the authoring dev read the changelog — same source now.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-20 10:35:29 +02:00
committed by GitHub
co-authored by Renn F
parent bf1012e952
commit 57b9e76b12
3 changed files with 57 additions and 2 deletions
+8 -2
View File
@@ -283,11 +283,17 @@ class ReleaseProposalService(BaseService):
publish). Off/no-creds is itself a no-op inside the engine. The publish). Off/no-creds is itself a no-op inside the engine. The
proposal task's project scopes the draft to the released project.""" 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 changelog_highlights, get_x_engine
# Prefer the curated CHANGELOG's feature headlines over raw
# per-commit subjects — the latter made the announcement caption
# parrot the top commit ("docs: curate the Unreleased body…").
highlights = changelog_highlights(report.drafted_changelog) or list(
report.change_summary
)
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=highlights,
project_id=project_id, project_id=project_id,
) )
except Exception as exc: except Exception as exc:
+25
View File
@@ -103,6 +103,31 @@ def _fallback_release_body(
return f"{product_name} v{version} is out: {lead}" return f"{product_name} v{version} is out: {lead}"
# Bold feature leads in a Keep-a-Changelog release body: "- **Headline (#N).**"
_CHANGELOG_LEAD_RE = re.compile(r"^- \*\*(?P<lead>.+?)\*\*", re.MULTILINE)
_CHANGELOG_PR_REF_RE = re.compile(r"\s*\(#\d+(?:,\s*#\d+)*\)")
def changelog_highlights(entry: str, *, limit: int = 8) -> list[str]:
"""Human-readable feature headlines from a curated CHANGELOG release body.
The release drafter's ``change_summary`` is raw per-commit subjects
("docs: curate the full 0.26.0 Unreleased body (#601)") — feeding those to
the announcement model produces a lame parroted caption. The curated
changelog's bold leads ARE the feature story ("Telegram Mini App V5 — brand
voice…"), so use those instead. Pure + best-effort: a body with no bold
leads yields an empty list and the caller falls back to change_summary.
"""
out: list[str] = []
for m in _CHANGELOG_LEAD_RE.finditer(entry):
lead = _CHANGELOG_PR_REF_RE.sub("", m.group("lead")).strip().rstrip(".").strip()
if lead:
out.append(lead)
if len(out) >= limit:
break
return out
def _release_prompt( def _release_prompt(
version: str, highlights: list[str], voice: str, product_name: str version: str, highlights: list[str], voice: str, product_name: str
) -> str: ) -> str:
+24
View File
@@ -1398,3 +1398,27 @@ async def test_draft_release_post_uses_project_name_when_set(
assert body is not None assert body is not None
assert "Acme Robotics" in body assert "Acme Robotics" in body
assert "RoboCo" not in body assert "RoboCo" not in body
def test_changelog_highlights_extracts_feature_headlines() -> None:
entry = (
"## [0.26.0] - 2026-07-20\n\n"
"### Security\n\n"
"- **Orchestrator API is off the public internet (GHSA-4f7g).** Both "
"composes published :8000 on 0.0.0.0.\n\n"
"### Added\n\n"
"- **Telegram Mini App V5 — brand voice and an operations ring (#583).** "
"Share Tech Mono becomes the display face.\n"
"- **Forge program: GitHub, Gitea, and GitLab (#575, #581).** One API.\n"
)
hl = x_engine_module.changelog_highlights(entry)
assert hl[0] == "Orchestrator API is off the public internet (GHSA-4f7g)"
assert hl[1] == "Telegram Mini App V5 — brand voice and an operations ring"
assert hl[2] == "Forge program: GitHub, Gitea, and GitLab"
# No raw commit-subject noise, no trailing PR refs or periods.
assert all("#" not in h.split("(GHSA")[0] for h in hl)
def test_changelog_highlights_empty_on_no_leads() -> None:
body = "## [0.26.0]\n\nplain prose, no bold leads\n"
assert x_engine_module.changelog_highlights(body) == []