From 97abf82cac9c634056f329ac6efd6e4c220c6019 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 21 Jul 2026 01:46:12 -0400 Subject: [PATCH] fix: changelog extractor drops redundant version-heading lines aggregate_changelog.py rendered each release as the tag line followed by the release body's first line. GitHub release bodies open with a heading that only restates the version (e.g. "## v1.7.1 (2026-07-19)"), so the changelog page echoed a duplicate "## vX" heading with no notes, which also corrupted the page's heading hierarchy (audit P2-7). Add summarize_body(): skip a leading heading that merely restates the version, strip leading "#" so the first real note renders inline, and emit nothing when the body carries no notes beyond the version header. Widen the fetched body window (200 -> 400 chars) so real notes below the header survive truncation. Covered by four new tests. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM --- scripts/aggregate_changelog.py | 33 +++++++++++++++++++++--- tests/test_aggregate_changelog.py | 42 ++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/scripts/aggregate_changelog.py b/scripts/aggregate_changelog.py index 190ef29..91928f2 100644 --- a/scripts/aggregate_changelog.py +++ b/scripts/aggregate_changelog.py @@ -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) @@ -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" diff --git a/tests/test_aggregate_changelog.py b/tests/test_aggregate_changelog.py index 9e99139..59f04b5 100644 --- a/tests/test_aggregate_changelog.py +++ b/tests/test_aggregate_changelog.py @@ -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): @@ -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