chore: unwrap model-drafted changelog highlights
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 964754664
This commit is contained in:
committed by
Copybara-Service
parent
370027a770
commit
3d3daffd8d
@@ -24,7 +24,9 @@ newest version section, in order:
|
||||
landed under several commits), and lowercase the leading word so entries
|
||||
read as consistent imperative phrases.
|
||||
2. Draft a short "Highlights" block with Gemini and place it above the fold, so
|
||||
a reader grasps the release in a handful of bullets.
|
||||
a reader grasps the release in a handful of bullets. The drafted prose is
|
||||
unwrapped to one line per paragraph and per bullet, because GitHub renders a
|
||||
single newline as a line break.
|
||||
3. For large releases, collapse the full categorized list under a ``<details>``
|
||||
fold so the notes read short while remaining a complete record.
|
||||
|
||||
@@ -62,6 +64,11 @@ _MENTION_RE = re.compile(r"\[@([\w-]+)\]\(https://github\.com/\1\)")
|
||||
_LEAD_RE = re.compile(
|
||||
r"(?P<head>\s*\* (?:\*\*[^*]+\*\* )?)(?P<first>\w+)(?P<rest>.*)", re.S
|
||||
)
|
||||
# Opens a list item. A wrapped continuation is joined onto one of these.
|
||||
_LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+] |\d+\. )")
|
||||
# Lines whose meaning depends on standing alone: headers, table rows, block
|
||||
# quotes, code fences. A wrapped continuation is never joined onto one.
|
||||
_STANDALONE_RE = re.compile(r"^\s*(?:#{1,6} |[|>]|```)")
|
||||
|
||||
# Inserted verbatim when the model is unavailable, so the release manager has a
|
||||
# scaffold to fill in by hand. Mirrors the format the model is asked to produce.
|
||||
@@ -96,7 +103,8 @@ Write a short Highlights section so a reader can grasp the release at a glance:
|
||||
the bullets, each with a one-line migration note.
|
||||
|
||||
Output ONLY the markdown body. Do NOT include the "### Highlights" header and do
|
||||
NOT wrap the output in code fences.
|
||||
NOT wrap the output in code fences. Put each paragraph and each bullet on a
|
||||
single line, however long; do not hard-wrap them.
|
||||
|
||||
Changelog for the new version:
|
||||
|
||||
@@ -206,9 +214,35 @@ def _draft_highlights(section_text: str, *, model: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _unwrap_lines(text: str) -> str:
|
||||
"""Joins each hard-wrapped paragraph and list item back onto one line.
|
||||
|
||||
Markdown treats a wrapped paragraph as one paragraph, but GitHub does not:
|
||||
in a pull request body or a release note it renders every newline as a line
|
||||
break, so prose a model wrapped at 80 columns breaks mid-sentence. Blank
|
||||
lines, headers, table rows, quotes and fenced blocks keep their own lines.
|
||||
"""
|
||||
out: list[str] = []
|
||||
fenced = False
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("```"):
|
||||
fenced = not fenced
|
||||
out.append(line.rstrip())
|
||||
elif fenced or not stripped:
|
||||
out.append(line.rstrip())
|
||||
elif _LIST_ITEM_RE.match(line) or _STANDALONE_RE.match(line):
|
||||
out.append(line.rstrip())
|
||||
elif out and out[-1].strip() and not _STANDALONE_RE.match(out[-1]):
|
||||
out[-1] = f"{out[-1]} {stripped}"
|
||||
else:
|
||||
out.append(stripped)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _build_block(body: str) -> str:
|
||||
"""Wraps a model-drafted body in the Highlights header."""
|
||||
body = body.strip()
|
||||
body = _unwrap_lines(body).strip()
|
||||
if body.startswith(_HIGHLIGHTS_HEADER):
|
||||
body = body[len(_HIGHLIGHTS_HEADER) :].lstrip("\n")
|
||||
return f"{_HIGHLIGHTS_HEADER}\n\n{body}\n"
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the release changelog curation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
_SCRIPT = (
|
||||
pathlib.Path(__file__).parent.parent.parent
|
||||
/ "scripts"
|
||||
/ "curate_changelog.py"
|
||||
)
|
||||
_SPEC = importlib.util.spec_from_file_location("curate_changelog", _SCRIPT)
|
||||
curate_changelog = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules["curate_changelog"] = curate_changelog
|
||||
_SPEC.loader.exec_module(curate_changelog)
|
||||
|
||||
|
||||
def test_unwrap_joins_a_wrapped_paragraph():
|
||||
text = "A release about\ncorrectness and hardening."
|
||||
|
||||
assert (
|
||||
curate_changelog._unwrap_lines(text)
|
||||
== "A release about correctness and hardening."
|
||||
)
|
||||
|
||||
|
||||
def test_unwrap_joins_a_wrapped_list_item():
|
||||
text = "* **Tools**: a tool response now carries\nimages back to the model."
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == (
|
||||
"* **Tools**: a tool response now carries images back to the model."
|
||||
)
|
||||
|
||||
|
||||
def test_unwrap_keeps_separate_list_items_apart():
|
||||
text = "* first item\n* second item\n- third item"
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == text
|
||||
|
||||
|
||||
def test_unwrap_keeps_numbered_list_items_apart():
|
||||
text = "1. first step\n2. second step"
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == text
|
||||
|
||||
|
||||
def test_unwrap_keeps_blank_lines_and_headers_on_their_own_lines():
|
||||
text = "the theme.\n\n#### Breaking changes\n\n* **X**: migrate by doing Y."
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == text
|
||||
|
||||
|
||||
def test_unwrap_leaves_a_fenced_block_alone():
|
||||
text = "install it:\n\n```bash\nuv pip install google-adk\nadk web\n```"
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == text
|
||||
|
||||
|
||||
def test_unwrap_does_not_join_a_paragraph_onto_a_closing_fence():
|
||||
text = "```bash\nadk web\n```\nthen open the\nbrowser."
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == (
|
||||
"```bash\nadk web\n```\nthen open the browser."
|
||||
)
|
||||
|
||||
|
||||
def test_unwrap_does_not_join_a_paragraph_onto_a_table_row():
|
||||
text = "| a | b |\nnot a table cell."
|
||||
|
||||
assert curate_changelog._unwrap_lines(text) == text
|
||||
|
||||
|
||||
def test_build_block_unwraps_drafted_prose():
|
||||
drafted = "A release about\ncorrectness.\n\n* **Tools**: return\nmedia."
|
||||
|
||||
block = curate_changelog._build_block(drafted)
|
||||
|
||||
assert block == (
|
||||
"### Highlights\n\nA release about correctness.\n\n"
|
||||
"* **Tools**: return media.\n"
|
||||
)
|
||||
Reference in New Issue
Block a user