Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions scripts/aggregate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,38 @@ def fetch_releases(github_slug, per_page=5):
return []
return [
{"tag": r["tag_name"], "date": r["published_at"][:10],
"url": r["html_url"], "body": (r.get("body") or "").strip()[:200]}
"url": r["html_url"], "body": (r.get("body") or "").strip()[:400]}
for r in resp.json() if not r.get("draft")
]


def summarize_body(body, tag=""):
"""Return a one-line, heading-free summary of a release body.

GitHub release bodies frequently open with a heading that only restates the
version (for example ``## v1.7.1 (2026-07-19)``). Injected verbatim, that
line duplicates the tag we already print and its leading ``#`` corrupts the
changelog page's heading hierarchy. Skip such redundant leading headings and
return the first real line of notes as inline (heading-free) text. Returns
an empty string when the body carries no notes beyond the version header.
"""
tag_norm = tag.lstrip("vV").strip()
for raw in body.splitlines():
line = raw.strip()
if not line:
continue
text = line.lstrip("#").strip()
if not text:
continue
if line.startswith("#"):
heading = text.lstrip("vV").strip()
# A leading heading that merely restates the version is noise.
if tag and (tag in text or (tag_norm and heading.startswith(tag_norm))):
continue
return text
return ""


def aggregate(repos=None, docs_dir=None):
repos = repos or load_repos()
docs_dir = pathlib.Path(docs_dir or DOCS_DIR)
Expand All @@ -44,8 +71,8 @@ def aggregate(repos=None, docs_dir=None):
lines.append(f"\n## {repo['name']}\n")
for r in releases:
lines.append(f"- **[{r['tag']}]({r['url']})** ({r['date']})")
if r["body"]:
summary = r["body"].split("\n")[0]
summary = summarize_body(r["body"], r["tag"])
if summary:
lines.append(f" {summary}")

out_path = docs_dir / "changelog.md"
Expand Down
42 changes: 41 additions & 1 deletion tests/test_aggregate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "scripts"))

from aggregate_changelog import aggregate, fetch_releases
from aggregate_changelog import aggregate, fetch_releases, summarize_body


def test_aggregate_writes_changelog(tmp_path, mocker):
Expand Down Expand Up @@ -70,6 +70,46 @@ def test_aggregate_handles_no_releases(tmp_path, mocker):
assert (tmp_path / "changelog.md").exists()


def test_summarize_body_skips_redundant_version_heading():
"""A leading heading that only restates the version is dropped, and the
first real note is returned as heading-free inline text."""
body = "## v1.7.1 (2026-07-19)\n\nFixed halt-on-drift regression"
assert summarize_body(body, "v1.7.1") == "Fixed halt-on-drift regression"


def test_summarize_body_version_only_returns_empty():
"""When the body is nothing but the version header, there is no note to
render, so aggregate should append nothing rather than echo the heading."""
assert summarize_body("## v1.7.1 (2026-07-19)", "v1.7.1") == ""


def test_summarize_body_strips_leading_hash_from_first_note():
"""A real leading heading (not a version restatement) is rendered inline so
it does not disrupt the changelog page's heading hierarchy."""
assert summarize_body("# Highlights\nStuff", "v2.0.0") == "Highlights"


def test_aggregate_drops_duplicate_version_heading(tmp_path, mocker):
"""End-to-end: the changelog must not contain an echoed `## vX` heading
line, which previously duplicated the tag and broke heading hierarchy."""
mock_releases = [
{"tag_name": "v1.7.1", "published_at": "2026-07-19T00:00:00Z",
"html_url": "https://github.com/OpenAdaptAI/test/releases/v1.7.1",
"body": "## v1.7.1 (2026-07-19)\n\nHalt-on-drift fix", "draft": False},
]
mock_resp = mocker.Mock()
mock_resp.status_code = 200
mock_resp.json.return_value = mock_releases
mocker.patch("aggregate_changelog.requests.get", return_value=mock_resp)

repos = [{"name": "test-pkg", "github": "OpenAdaptAI/test-pkg", "changelog": True}]
aggregate(repos=repos, docs_dir=tmp_path)
content = (tmp_path / "changelog.md").read_text()

assert "## v1.7.1 (2026-07-19)" not in content
assert "Halt-on-drift fix" in content


def test_fetch_releases_handles_http_error(mocker):
mock_resp = mocker.Mock()
mock_resp.status_code = 500
Expand Down