Skip to content

ci(release): compose the release page — download button first, BREAKING, highlights, grouped changelog - #2248

Merged
Chris0Jeky merged 5 commits into
mainfrom
issue-2234/release-notes-composer
Aug 30, 2026
Merged

ci(release): compose the release page — download button first, BREAKING, highlights, grouped changelog#2248
Chris0Jeky merged 5 commits into
mainfrom
issue-2234/release-notes-composer

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Closes #2234

The release page stops being GitHub's flat auto-changelog with the download buried under the asset table. scripts/ci/compose-release-notes.mjs renders the body the workflow passes to --notes-file: download button first, then ## Breaking changes from the tag's own UPGRADING.md section, ## Highlights from docs/releases/notes/<tag>.md, then ## What's changed from releases/generate-notes grouped through a new .github/release.yml.

What changed

File Change
scripts/ci/compose-release-notes.mjs New. Pure composeReleaseNotes() + CLI. Renders the body; decides the missing-source policy.
scripts/ci/compose-release-notes.test.mjs New. 31 node:test cases, including four CLI round trips through the real filesystem.
.github/release.yml New. Changelog grouping (bug fixes / security / features / UI / perf / docs / CI+packaging / dependencies) with a "*" catch-all so no merged PR disappears.
.github/workflows/release-desktop.yml New compose-notes job; create-release consumes its artifact, --notes-file on create, re-asserted on publish.
scripts/ci/release-desktop-dispatch.test.mjs +9 structural cases (66 → 75); the checkout count and the per-job checkout loop now include compose-notes.
docs/ops/RELEASE_TRUST_AND_DISTRIBUTION.md New "Release page layout" subsection under Release contract.

Design notes worth a reviewer's attention

  • Why a separate job. The composer needs the built SHA-256, so it cannot live in resolve-source; and create-release is gated on publish == 'true', so putting it there would leave the rehearsal path with nothing to preview. compose-notes needs [resolve-source, build-backend] and has no if: guard — a no-publish dispatch renders the page and uploads it.
  • Artifact name. The preview artifact is release-page-notes, deliberately outside the release-* pattern… This was false and is fixed in fix round 1 (c3d3b5fb4): minimatch's release-* DOES match release-page-notes, so with merge-multiple: true the rendered Markdown would have landed in release-assets/ and been published as a stray asset beside the ZIP. The artifact is now composed-page-body, downloaded by exact name, and the string assertion is replaced by a property test that parses the workflow's own pattern: and name: values and evaluates them with node:path's matchesGlob — including a self-check that the old name really did match. See the Fix round 1 comment.
  • Idempotency (gh release create is not resumable: a mid-upload failure strands the release (observed on v0.1.0) #1806). gh release edit --draft=false … --notes-file re-asserts the body in the same edit that clears the draft, so an adopted draft from an earlier attempt (possibly created before this composer existed) is brought to this run's body. The --prerelease array and the notes file travel together, so the [Release] RC tags publish as a full release: no --prerelease on the GitHub Release and GHCR latest moves to the RC #2217 "never briefly a full release" property is unchanged.
  • Asset name. taskdeck-${RELEASE_TAG}-win-x64.zip — the tag with its leading v, matching what build-backend actually packages (Compress-Archive … "taskdeck-${env:RELEASE_TAG}-${env:RELEASE_RID}.zip"). The issue text said "tag without the leading v"; the workflow disagrees and the workflow wins.
  • Fail-closed policy. Stable tag + missing UPGRADING.md section or missing highlights file ⇒ exit 1, nothing written, nothing published. Release candidate ⇒ ::warning:: plus a documented fallback. A missing or filename-mismatched checksum fails either way — publishing a digest for a different file is worse than publishing none.
  • Hardening. No ${{ }} reaches bash: RELEASE_TAG / RELEASE_PRERELEASE / RELEASE_PUBLISH come through env: from resolve-source (already grammar-checked there); the repo comes from ${GITHUB_REPOSITORY}. compose-notes needs contents: write only because releases/generate-notes is a POST that a read-only token is refused — it performs no writes.

Rendered sample — v0.3.0-rc.1

Rendered locally by the real CLI against the real UPGRADING.md from docs/v030-rc1-pretag (PR #2245) and a real POST /releases/generate-notes (previous_tag_name=v0.2.0). 10,800 bytes, exit 0. The SHA-256 and the highlights bullets are stand-ins — the digest comes from the build and the highlights file is #2245's deliverable.

Composed release body (abridged in the changelog section only)
[![Download Taskdeck v0.3.0-rc.1 for Windows x64](https://img.shields.io/badge/Download-v0.3.0--rc.1_%C2%B7_Windows_x64_ZIP-2f81f7?style=for-the-badge&logo=windows&logoColor=white)](https://github.com/Chris0Jeky/Taskdeck/releases/download/v0.3.0-rc.1/taskdeck-v0.3.0-rc.1-win-x64.zip)

> **Release candidate** — upgrade notes below; not the Latest release.

**Windows 10/11 x64 · portable ZIP · no installer.** Unzip anywhere and run `Taskdeck.Api.exe`.

- **File:** `taskdeck-v0.3.0-rc.1-win-x64.zip`
- **SHA-256:** `3b1f9c2e7a45d8061c3fe2b90a7d4165e8c93f207bd4a6e15c082f39d7be4a10`
- **First run:** [QUICK_START.md](https://github.com/Chris0Jeky/Taskdeck/blob/v0.3.0-rc.1/docs/releases/WINDOWS_QUICK_START.md) — the same file ships inside the ZIP.

Verify the download before running it:

    Get-FileHash .\taskdeck-v0.3.0-rc.1-win-x64.zip -Algorithm SHA256

## Breaking changes

**BREAKING: yes, for two kinds of integration** — scripts that create API keys through the API or
CLI must now name explicit scopes, and clients that treated a non-null capture `errorMessage` as
"the capture failed" must key on the status instead. Ordinary Windows and Compose users have no
manual step: both schema migrations run automatically after the pre-migration snapshot. This is a
**release candidate**: the GitHub Release is flagged *Pre-release*, it is not shown as *Latest*, and
the container lane publishes only `ghcr.io/chris0jeky/taskdeck:0.3.0-rc.1``:latest` and `:0.3`
stay where they were.

  … 55 further lines, lifted verbatim from the `## v0.3.0-rc.1 …` section of UPGRADING.md
    and stopping exactly at the `## v0.2.0 — 2026-08-29` heading …

## Highlights

- **Accountable agents.** Every proposal carries the transcript span its evidence came from.
- **API keys carry explicit scopes.** New API, UI, and CLI keys pick one or more of `read`, `propose`, and `manage`.

## What's changed

* Focus Paper Review decision receipts by @Chris0Jeky in https://github.com/Chris0Jeky/Taskdeck/pull/2154
* Clarify MCP hash approval invariant scope by @Chris0Jeky in https://github.com/Chris0Jeky/Taskdeck/pull/2156~70 further PR lines …

* feat(board): add accessible column collapse by @Chris0Jeky in https://github.com/Chris0Jeky/Taskdeck/pull/2165

**Full Changelog**: https://github.com/Chris0Jeky/Taskdeck/compare/v0.2.0...v0.3.0-rc.1

Note on the sample's changelog: it is ungrouped because .github/release.yml is read from the repository's default branch, and this PR is what puts it on main. Once merged, generate-notes returns the same PR list under ### Bug fixes / ### Features / … headings. The composer passes that grouping through unchanged (the generated changelog is passed through with its compare link covers it against a grouped fixture).

Verification

All run in the worktree C:\Users\jekyt\source\wt-2234 at head 9bf8797a7.

Command Result
node --test scripts/ci/compose-release-notes.test.mjs 31 pass / 0 fail (240 ms)
node --test scripts/ci/release-desktop-dispatch.test.mjs (Git for Windows bash first on PATH) 75 pass / 0 fail (2.3 s) — was 66 before this PR
node scripts/check-docs-governance.mjs Docs governance check passed.
python -c "yaml.safe_load(...)" on both YAML files jobs [resolve-source, build-frontend, build-backend, compose-notes, create-release]; 9 changelog categories parsed
CLI render of v0.3.0-rc.1 against real UPGRADING.md + real generate-notes exit 0, 10,800 bytes, download button on line 1

Mutation checks

Every new test was proven to fail against a deliberately broken source, then the source was restored and re-run green.

Composer (compose-release-notes.mjs):

Mutation Tests that failed
sections.joinsections.reverse().join (button no longer first) 5, incl. the download link is the first line of the body, sections run download, breaking changes, highlights, changelog, and 2 CLI round trips
if (requireCuratedSources) errors.pushif (false) (stable-tag policy downgraded to a warning) 3: both … is an ERROR for a stable tag cases + the CLI exits non-zero and writes NOTHING …
shields dash doubling '--''-' 1: the badge escapes shields.io separators so an RC tag renders in full
if (name !== assetName)if (false) (checksum may name another file) 1: a checksum naming a different file is refused …
heading match → bare text.startsWith(tag) 2: a version heading matches on a tag boundary, never a bare prefix + a missing UPGRADING section is an ERROR for a stable tag

Workflow (release-desktop.yml):

Mutation Tests that failed
notes path drifts to release-notes/other.md the release body is the composed file on create AND re-asserted on publish
preview artifact renamed release-page-notesrelease-notes (inside the release-* asset glob) that same test + the rendered notes upload as a run artifact outside the release-* asset pattern
if [ "${RELEASE_PUBLISH}" = "true" ]if true generate-notes is attempted only when a tag actually exists

Restored source: git diff clean against the committed files, both suites green.

NOT verified

  • The real publish path. gh release create --notes-file / gh release edit --notes-file, the download-artifact handoff between compose-notes and create-release, the live generate-notes call inside Actions, and the adopt-path re-assertion can only be proven by an actual tag push. The structural tests pin the strings; they do not execute the workflow.
  • The rehearsal artifact is the pre-tag proof — dispatch Release Desktop from this branch with the tag input blank, then read the release-page-notes artifact (and the job summary, which prints the body). That has not been run here.
  • actionlint / shellcheck were not run locally (the Workflow Lint CI job covers them). The new bash avoids literal backticks in single-quoted printf formats, which is the SC2016 trap this workflow has hit before.
  • shields.io rendering of the percent-encoded · in the badge message was not loaded in a browser.
  • How the composed Markdown actually renders on a GitHub release page (badge sizing, the powershell fence, the lifted UPGRADING.md anchors such as #automatic-pre-migration-backups, which will not resolve on a release page) was not eyeballed.
  • The last item is a known cosmetic wart: relative in-document anchors inside the lifted UPGRADING section are dead links on the release page. Worth a follow-up, not a merge blocker.

Residuals

Human-action file

OUTSTANDING_TASKS.md is untouched by this PR. Its open [ ] items are unchanged; nothing here closes one.

Renders the GitHub Release body the workflow will pass to --notes-file:
the download button first (shields.io for-the-badge image linked to the
deterministic asset URL), the SHA-256, the in-zip quick start link and an
RC banner, then Breaking changes from the tag's UPGRADING.md section,
Highlights from docs/releases/notes/<tag>.md, then the generated,
grouped changelog.

Curated sources are required for a stable tag (a missing UPGRADING
section or notes file fails the run) and advisory for a release
candidate (warning plus a documented fallback).

Refs #2234
…e-notes

A new compose-notes job renders the page body from the resolved tag, the
built checksum and the tag's own docs, and uploads it as the
release-page-notes artifact — so a no-publish rehearsal dispatch previews
the exact page before any tag is cut.

create-release downloads that artifact, refuses an empty or button-less
body, passes it to gh release create --notes-file, and re-asserts it in
the same gh release edit that clears the draft flag. That re-assertion is
what keeps the resumable adopt path (#1806) idempotent: a draft left by an
earlier attempt is brought to this run's body instead of keeping a stale
one.

.github/release.yml groups the generated changelog into bug fixes,
features, docs, CI and dependencies, with a catch-all so no merged PR
disappears.

Refs #2234
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T01:09:55.487737Z 3a62d07 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bf8797a7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release-desktop.yml Outdated
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review record (global law 2) — round 1

Fresh-context independent review (Opus, read-only + node --test on the composer suite: 31/31): FIX FIRST.

Blocking (HIGH): the release-page-notes artifact name is matched by create-release's download-artifact pattern: release-* (minimatch: release- + page-notes), and compose-notes is a needs of that job, so release-notes.md is extracted into release-assets/ and the upload loop (assets=(release-assets/*)) publishes it as a stray asset on the release page — the v0.1.0-class wrong-asset-set the PR says it prevents. The dispatch test asserts only doesNotMatch(/name: release-notes\b/), so it passes while the invariant is false. Fix in this round: rename outside the glob, download by name, replace the literal-string test with a property test (real glob match of the parsed artifact name against the parsed pattern:), mutation-checked by renaming back.

MEDIUM (taken in the same round — wrong page content, cheap): generate-notes is called with only tag_name; the rendered sample was produced with previous_tag_name=v0.2.0 passed by hand, so it does not evidence the workflow's output, and after rc.1 exists the stable v0.3.0 page could cover only rc.1 → final. Fix: resolve the previous non-prerelease tag and pass previous_tag_name; bounded retry on the call to match the asset-upload hardening.

LOW, taken (one-liner): 120,000-character body guard (GitHub caps at 125,000).

LOW, recorded on #2234 (not fix-cycled): a no-publish rehearsal is always a prerelease shape (v0.0.0-dryrun+<sha7>), so it can never exercise the stable fail-closed path — a missing ## v0.3.0 UPGRADING section is caught only at tag time; extractUpgradingSection stops at any #/## line, including one inside a fenced block (none in the shipped file).

Verified by the reviewer (no finding): no ${{ }} reaches any new shell body; untrusted content (UPGRADING, notes, generated JSON) is read only by Node and reaches gh via --notes-file; persist-credentials: false; CRLF normalised in all three parsers; asset name matches the workflow's real taskdeck-<TAG>-win-x64.zip (leading v); checksum regex handles sha256sum and shasum forms; heading match is a boundary match; the publish edit is one call so #2217's never-briefly-full-release property holds and the #1806 adopt path gets this run's body.

After the fix push: aging resets; one scoped verification of the fix diff; Codex triaged by the same bar.

… changelog base

Review round 1 on #2248.

HIGH — the preview artifact was named release-page-notes, which minimatch's
release-* pattern in create-release DOES match: with merge-multiple the
rendered Markdown would have landed in release-assets/ and been published
as a stray asset beside the ZIP. The PR's claim that the name sat outside
the pattern was false. Renamed to composed-page-body, and the decorative
string assertion is replaced by a property test that parses the workflow's
own pattern: and name: values and evaluates them with node:path matchesGlob
- including a self-check that the old name really did match.

MEDIUM — generate-notes was POSTed with only tag_name, so GitHub inferred
the changelog base. Once v0.3.0-rc.1 exists the stable v0.3.0 page would
have covered only rc.1 to final. The base is now the newest published,
non-draft, non-prerelease release, passed as previous_tag_name; a first
release omits the field and says so. The call gets the same bounded 3-try
10/20s retry as the asset upload and fails closed when exhausted.

LOW — the composer errors when the composed body exceeds 120,000 characters,
naming the overflow, instead of letting gh reject the publish at GitHub's
125,000 cap after the assets are already built.

Refs #2234
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Fix round 1 — c3d3b5fb4

All three findings addressed in one commit. The PR body's false "outside the pattern" sentence is struck through and corrected in place.

HIGH — the preview artifact WAS inside the asset glob

Confirmed, and the PR body's claim was wrong. actions/download-artifact matches pattern: with minimatch, so release-* matches release-page-notes (release- + page-notes). Measured directly:

node -e "const p=require('node:path'); console.log(p.matchesGlob('release-page-notes','release-*'))"  // true

compose-notes is a needs: of create-release, so with merge-multiple: true the rendered release-notes.md would have landed in release-assets/ and assets=(release-assets/*) would have published it as a stray asset beside the ZIP.

  • The artifact is now composed-page-body (matchesGlob('composed-page-body', 'release-*') === false), downloaded in create-release by exact name:, never by pattern.
  • The decorative assert.doesNotMatch(/name: release-notes\b/) — which could not have caught this — is replaced by a property test. It parses the workflow's own name: (from compose-notes' upload-artifact) and pattern: (from create-release' asset download-artifact) out of the YAML text and evaluates them with node:path's matchesGlob. It also asserts matchesGlob('release-win-x64', pattern) === true and matchesGlob('release-page-notes', pattern) === true as a self-check, so a broken or always-false matcher cannot make the test pass, and it asserts the upload and download name the same artifact.
  • The workflow comment that stated the false invariant is rewritten to state the minimatch mechanism and why the name must stay out of the glob.

MEDIUM — the changelog base was inferred

generate-notes was POSTed with only tag_name. Once v0.3.0-rc.1 exists, GitHub's inferred base for the stable v0.3.0 page would be rc.1, hiding everything since v0.2.0 from exactly the readers the stable page is for.

The base is now resolved explicitly:

previous_tag="$(gh release list --repo "${GITHUB_REPOSITORY}" \
  --exclude-pre-releases --exclude-drafts --limit 1 \
  --json tagName --jq '.[0].tagName // empty')"

— newest published, non-draft, non-prerelease release (gh release list returns newest-first), passed as -f previous_tag_name=…. Two paths omit the field and log why: no earlier stable release at all (first release), and a re-run after this tag was already published stable (it would otherwise be its own base, an empty self-comparison). The call now carries the same bounded retry as the asset-upload loop — 3 attempts, 10 s / 20 s backoff — and fails closed with exit 1 when exhausted rather than publishing a changelog-less page.

LOW — body-length guard

MAX_RELEASE_BODY_LENGTH = 120000 (GitHub's cap is 125,000). Over it, the composer errors naming the actual length, the overflow, and the two files to trim, instead of letting gh release create reject the publish with a 422 after the assets are already built.

Verification

Head c3d3b5fb4, worktree C:\Users\jekyt\source\wt-2234.

Command Result
node --test scripts/ci/compose-release-notes.test.mjs 33 pass / 0 fail (was 31)
node --test scripts/ci/release-desktop-dispatch.test.mjs (Git for Windows bash first on PATH) 76 pass / 0 fail (was 75)
node scripts/check-docs-governance.mjs Docs governance check passed.
yaml.safe_load on the workflow jobs [resolve-source, build-frontend, build-backend, compose-notes, create-release]

Mutation checks on the new tests

Each mutation applied, suite run, source restored, suite re-run green.

Mutation Went red
artifact renamed back to release-page-notes the composed-body artifact name cannot be swept into the release asset download + the release body is the composed file on create AND re-asserted on publish
generate_args+=(-f "previous_tag_name=…")generate_args+=() the changelog base is the newest published stable release, with bounded retries
--exclude-pre-releases dropped from the base lookup same test
if (body.length > MAX_RELEASE_BODY_LENGTH)if (false) a body over the GitHub release-body limit fails, naming the overflow

The requested rename-back mutation is the first row: it goes red, so the property test is load-bearing.

Not changed, by instruction

The rehearsal-always-prerelease behaviour (a v0.0.0-dryrun+<sha7> tag contains -, so the preview always shows the RC banner) and the fenced-code heading truncation are recorded on #2234 and deliberately untouched here.

Still NOT verified

Unchanged from the PR body: the real publish path, the live generate-notes call and its new retry/base resolution, and the download-artifact handoff can only be proven by a real tag push or a rehearsal dispatch. Neither has been run. actionlint / shellcheck were not run locally — the new bash adds an array, a for retry loop and a sleep, all mirroring the existing asset-upload block, and avoids literal backticks in single-quoted printf formats (the SC2016 trap this workflow has hit before).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3d3b5fb44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release-desktop.yml
Comment thread .github/workflows/release-desktop.yml
Comment thread .github/workflows/release-desktop.yml
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Fix-diff verification (fresh context, read-only + node --test in a scratch worktree) on c3d3b5fb4: FIX VERIFIED. The composed body is uploaded as composed-page-body and downloaded by exact name into release-notes/ while assets stay on pattern: release-*release-assets/ (release-desktop.yml:704-709, :754-767); the property test reads the real workflow and evaluates the real name: against the real pattern: with path.matchesGlob (Node 24.13.1 pinned in four places), with a self-check that the old name DOES match — renaming back reddens exactly 2 tests (76 → 74/2). previous_tag_name flows through a bash array from the API response (never from inputs), is omitted and printed on the first stable release, and the 3× retry fails closed. The 120,000 guard runs on the joined final body and pushes a hard error for RC and stable alike. Suites at this head: dispatch 76/76, composer 33/33. Non-blocking: the composer suite is not invoked by any workflow (tracked on #2235 — one node --test line in ci-required.yml); the new gh release list call is unretried under set -e (fails closed); release-date ordering vs semver is unreachable on the current single-line history. Review ceiling reached; merge when CI is green at this head after aging.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round 2 (ceiling). Codex's three new P2s are real edge cases outside the v0.3.0-rc.1 path — re-dispatching a pre-#2248 tag, rehearsal preview fidelity with the dryrun tag shape, and the changelog base when re-running an older tag after a newer stable exists — recorded and tracked as #2250 together with the ungated composer suite. Two rounds are done; ship at green CI after aging (merge commit).

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Base re-proof, not a fix. This PR's CI merge ref was computed against a90971162 (main before #2244 landed), so its E2E Smoke hit the pre-existing phone-width card-opener failure (#2232, fixed on main at 2807c0b5c) — the PR touches none of those files. A gh run rerun would reuse the original merge ref, so the branch is updated from main instead; CI re-runs against the fixed base and the aging window restarts. No review round is owed: the diff against main is unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a62d07019

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/ci/compose-release-notes.test.mjs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Release] Release page: download button + checksum at the top, BREAKING first, curated highlights and grouped changelog (replace bare --generate-notes)

1 participant