Compare commits

...

1 Commits

Author SHA1 Message Date
Daniel Lok 52cb175e54 feat(changelog): curate release notes to user-facing fixes, split breaking changes
Rework the "Draft release notes" summarizer so the generated highlights
stay user-facing. The drafter now excludes security fixes/hardening and
CI/build/tooling/internal churn from the bug-fixes section, and the
"Bug fixes & hardening" heading becomes plain "Bug fixes" (user-facing
bug fixes only — crashes, reliability, correctness).

Breaking changes get their own section rather than being lumped in with
bug fixes, ordered Features -> Breaking changes -> Bug fixes. An empty
Breaking changes section is omitted entirely by the LLM drafter.

Updates the mechanical scaffold (DRAFT_SECTIONS), the drafter agent
prompt, RELEASING.md, and the changelog tests to match.

Co-authored-by: Isaac
2026-07-03 14:37:11 +08:00
5 changed files with 59 additions and 32 deletions
@@ -4,8 +4,8 @@
# Given the list of PRs merged since the previous release (each PR's number,
# title, and the user-facing one-liner its author wrote in the PR template's
# `## Changelog` section) plus a deterministic mechanical scaffold, it synthesizes
# the concise, curated two-section release notes we write by hand today — collapsing
# many related PRs into a handful of themed highlights. It has NO tools and NO
# the concise, curated release notes we write by hand today — collapsing many
# related PRs into a handful of themed highlights. It has NO tools and NO
# sub-agents: it writes prose from the material it is handed, so a run is fast,
# cheap, and can't hang. The workflow drops its output into the GitHub Release
# DRAFT body; a human reviews and edits before publishing.
@@ -34,9 +34,9 @@ name: release-notes-drafter
description: >-
Synthesizes concise, curated GitHub Release notes from the list of PRs merged
since the previous release. Collapses related PRs into ~4-5 themed bullets under
two headings (Major new features; Bug fixes & hardening), in Omnigent's
release-notes voice, and emits them between RELEASE_NOTES markers. No tools, no
sub-agents — a pure synthesis turn.
three headings (Major new features; Breaking changes; Bug fixes — user-facing
only), in Omnigent's release-notes voice, and emits them between RELEASE_NOTES
markers. No tools, no sub-agents — a pure synthesis turn.
executor:
type: omnigent
@@ -48,7 +48,7 @@ prompt: |
given the list of pull requests merged since the previous release — each with its
number, title, and (when the author filled it in) the one-line user-facing
changelog entry from the PR template. You are also given a deterministic
MECHANICAL DRAFT that already groups every harvested entry into the two sections;
MECHANICAL DRAFT that already groups every harvested entry into sections;
treat it as raw material to curate, not a finished product.
Your job: write the concise, curated release notes a human would — collapsing many
@@ -64,7 +64,12 @@ prompt: |
- <highlight — collapse related PRs into one themed bullet> (#123, #456)
- <~4-5 bullets total>
## Bug fixes & hardening
## Breaking changes
- <what breaks and what the user must do about it> (#234)
- <omit this whole section — heading and all — if there are none>
## Bug fixes
- <highlight> (#789)
- <~3-5 bullets total>
@@ -77,6 +82,16 @@ prompt: |
the internal mechanics.
- GROUP aggressively: if six PRs add agent harnesses, that's ONE bullet naming a
few, not six bullets. Aim for ~4-5 bullets per section; drop pure-internal churn.
- "Breaking changes" is for changes that force users to act — removed/renamed
flags, changed defaults, dropped compatibility. Say what breaks and what to do.
If there are none, OMIT the whole section (heading included) — never emit an
empty section or a "none" placeholder.
- "Bug fixes" is USER-FACING ONLY: crash fixes, reliability, correctness, or
behaviour a user would notice. EXCLUDE and never highlight:
- Security fixes / hardening (don't advertise these — omit them entirely).
- CI, build, test, tooling, or release-plumbing fixes.
- Internal refactors, dependency bumps, and other under-the-hood churn.
When in doubt whether a fix is user-facing, leave it out.
- Append the contributing PR refs in parentheses at the end of each bullet:
`(#123, #456)`. Only cite PRs you were actually given.
- Keep Omnigent's voice: crisp, concrete, lightly technical. A tasteful leading
+13 -9
View File
@@ -173,24 +173,28 @@ def render_section(tag: str, date: str, results: list[HarvestResult]) -> str:
return "\n".join(lines).rstrip() + "\n"
# Two-section draft for the GitHub Release body: the Type-of-change tags collapse
# into the two buckets the release coordinator curates by hand (see RELEASING.md /
# Multi-section draft for the GitHub Release body: the Type-of-change tags collapse
# into the sections the release coordinator curates by hand (see RELEASING.md /
# the release-notes-drafter agent). This is the deterministic scaffold — the AI
# drafter refines it, and it is also the fallback when the LLM is unavailable.
# Values are "Type of change" checkbox labels (see _md.TYPE_TAGS).
DRAFT_SECTIONS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Major new features", ("Feature", "UI / frontend change")),
("Bug fixes & hardening", ("Bug fix", "Breaking change")),
("Breaking changes", ("Breaking change",)),
("Bug fixes", ("Bug fix",)),
)
def render_draft_notes(results: list[HarvestResult], repo: str) -> str:
"""Render the two-section curated-draft scaffold for the GitHub Release body.
"""Render the curated-draft scaffold for the GitHub Release body.
Groups documented PRs into "Major new features" and "Bug fixes & hardening"
by their Type-of-change labels, sorted by PR number, and appends the
CHANGELOG.md link. Empty sections keep their heading with a placeholder so
the coordinator sees what to fill in.
Groups documented PRs into the DRAFT_SECTIONS buckets (Major new features /
Breaking changes / Bug fixes) by their Type-of-change labels, sorted by PR
number, and appends the CHANGELOG.md link. The Bug fixes bucket is a raw
superset seeded from every "Bug fix"-tagged PR; the AI drafter curates it
down to user-facing fixes only, dropping security and CI/internal fixes
(which share the same tag). Empty sections keep their heading with a
placeholder so the coordinator sees what to fill in.
"""
included = [r for r in results if r.status == "included"]
@@ -348,7 +352,7 @@ def main() -> int:
parser.add_argument(
"--draft-notes-out",
default=None,
help="optional path to write the two-section curated-draft scaffold "
help="optional path to write the curated-draft scaffold "
"(the GitHub Release body seed / LLM fallback)",
)
parser.add_argument(
+3 -3
View File
@@ -7,9 +7,9 @@ name: Draft release notes
# 1. Open a PR to omnigent/main updating the granular CHANGELOG.md (harvested
# from each merged PR's "## Changelog" section), so the draft's
# "Full Changelog" link resolves before the release goes public.
# 2. Synthesize concise, curated two-section release notes (an Omnigent agent
# collapses the merged PRs into ~4-5 themed highlights per section) and drop
# them into the GitHub Release DRAFT body for the coordinator to edit.
# 2. Synthesize concise, curated release notes (an Omnigent agent collapses the
# merged PRs into ~4-5 themed highlights per section) and drop them into the
# GitHub Release DRAFT body for the coordinator to edit.
#
# Why `workflow_run` (not extending github-release.yml): that workflow is
# deliberately minimal — it runs NO project code, only `gh release create`, so a
+5 -4
View File
@@ -175,10 +175,11 @@ two workflows have already done the prep for you:
- `draft-release-notes.yml` (fires right after) then:
1. opened a **`CHANGELOG.md` PR to `main`** — the granular, feature-level log,
harvested mechanically from each merged PR's `## Changelog` section; and
2. **filled the draft's body** with concise, curated two-section notes (Major new
features / Bug fixes & hardening), synthesized by an agent from the merged
PRs, with the original auto-notes tucked into a collapsed `<details>` for
reference.
2. **filled the draft's body** with concise, curated notes (Major new features /
Breaking changes / Bug fixes — user-facing only), synthesized by an agent from
the merged PRs, with the original auto-notes tucked into a collapsed
`<details>` for reference. Security and CI/internal fixes are deliberately left
out of the highlights.
Now:
+16 -9
View File
@@ -280,7 +280,7 @@ def test_insert_dev_tag_idempotent() -> None:
_REPO = "omnigent-ai/omnigent"
def test_draft_notes_groups_into_two_sections_by_type() -> None:
def test_draft_notes_groups_into_sections_by_type() -> None:
results = [
_result(10, "a new capability", ["Feature"]),
_result(20, "moved a button", ["UI / frontend change"]),
@@ -289,13 +289,20 @@ def test_draft_notes_groups_into_two_sections_by_type() -> None:
]
notes = gen.render_draft_notes(results, _REPO)
assert "## Major new features" in notes
assert "## Bug fixes & hardening" in notes
# Feature/UI land in features; Bug fix/Breaking in hardening.
feat, hard = notes.split("## Bug fixes & hardening")
assert "## Breaking changes" in notes
assert "## Bug fixes" in notes
# Feature/UI land in features; Breaking and Bug fix each get their own section.
feat, rest = notes.split("## Breaking changes")
breaking, fixes = rest.split("## Bug fixes")
assert "a new capability (#10)" in feat and "moved a button (#20)" in feat
assert "a crash fix (#30)" in hard and "dropped a flag (#40)" in hard
# Features section comes first.
assert notes.index("## Major new features") < notes.index("## Bug fixes & hardening")
assert "dropped a flag (#40)" in breaking
assert "a crash fix (#30)" in fixes
# Sections appear in features → breaking → bug fixes order.
assert (
notes.index("## Major new features")
< notes.index("## Breaking changes")
< notes.index("## Bug fixes")
)
def test_draft_notes_has_full_changelog_footer() -> None:
@@ -306,9 +313,9 @@ def test_draft_notes_has_full_changelog_footer() -> None:
def test_draft_notes_empty_section_keeps_placeholder() -> None:
# Only a feature entry — the hardening section should still appear with a hint.
# Only a feature entry — the bug-fixes section should still appear with a hint.
notes = gen.render_draft_notes([_result(1, "x", ["Feature"])], _REPO)
assert "## Bug fixes & hardening" in notes
assert "## Bug fixes" in notes
assert "no entries harvested" in notes