feat: build "What's new" from the generated release notes - #2993
feat: build "What's new" from the generated release notes#2993jordan-simonovski wants to merge 7 commits into
Conversation
Replaces the full-changelog modal with an inline "What's new" section in the Help menu, a "View all releases" drawer, and a sparkle on the Help icon when the running version hasn't been acknowledged in this browser. The app no longer ships the whole, ever-growing CHANGELOG.md as a fetched asset. next.config.mjs parses the `feat` headlines out of the last five releases at build time and emits a small public/whats-new.json instead, optionally merging hand-authored hero cards from whats-new-highlights.json. The generated asset has to survive every build mode, so the parser and highlights are copied into both Docker builder stages as build inputs, and whats-new.json is rescued by name in the ClickStack static export (which strips .json wholesale to drop Next's route data). The parser is imported dynamically rather than statically: the prod image copies next.config.mjs on its own and re-evaluates it under `next start`, where no build sources exist, and a static import would crash the container on startup instead of falling through to the asset already generated at build time. The inline section shows the newest release that actually has features, not simply the newest release — fix-only patch releases are common (10 of 71 to date parse to zero feats) and would otherwise leave the section blank exactly when the Help icon is sparkling.
Main landed #2737, which pointed the changelog modal at the repo-root AI-written CHANGELOG.md. This branch deletes that modal, so the conflicts resolve to this branch's side throughout: the Docker stages and next.config.mjs keep generating public/whats-new.json from the app package changelog, and the modal's e2e specs give way to the What's new drawer's. Two of main's tests only existed to pin the modal's marker regex and link-host allowlist to release-notes.mjs. With no in-app renderer there is nothing left to pin, so they go, and the comments claiming the modal as the enforceable gate over model-authored changelog text now say what actually backs it: the checks in release-notes.mjs plus review of the release PR. The app eslint-disable baseline moves 144 -> 146. Main consumed the budget up to 144, so this branch's two suppressions (localStorage read after hydration, and a test importing the parser from scripts/) push past it; both are documented where they sit. whats-new-highlights.json still keys 2.31.0, which the merge pushes out of the five-release window — the hero card needs re-authoring for a current release.
The Help menu's "What's new" was built from the app's per-PR changesets, which have no notion of which change mattered: it keyed on a `feat:` prefix, so v2.36.0 showed three dashboard-variable entries and neither formulas nor the alert work the release was actually about. The hero card was hand-authored in whats-new-highlights.json, keyed to a version that aged out of the window without anyone noticing. It now reads the root CHANGELOG.md — the release-level summary written and reviewed during each release. Each release yields the headline and opening sentence the notes lead with, its breaking changes and new features as individual rows from the bolded lead-in of each bullet, and a count for every other section rather than a list. Releases predating the generated headline split their opening sentence at its colon, so the card leads with a headline rather than a paragraph either way. Nothing is written in the app. The summary is model-authored from changeset bodies and PR titles, so the drawer renders it with images dropped and link targets allowlisted at the AST level, the same policy release-notes.mjs greps for and a test pins the two together. Links point at the release's own tag, not main: merging does not deploy, so main's changelog describes releases a given deployment is not running, and mid-cycle the running version has no tag while every release in the payload does. whats-new-highlights.json and its Docker build inputs are gone with the hero it fed. A missing asset now fails the build on whether the asset exists rather than on NEXT_PHASE, keeping the hardening #2737 added.
…ibutors Two additions to the generated release notes, both consumed by the app's "What's new" panel. The notes now open with a one-line bolded headline. The panel needs a title for each release and had no source for one, so it either invented one by hand or led with a paragraph of prose; the release that wrote the notes is the only place that knows what the release was about. External contributions are now thanked where they land. `gh api` already resolved each changeset commit to a PR for the `(#NNN)` references, so the same call carries the author and their association with the repo — anyone with repo rights is us, everyone else contributed from outside, and bots are neither. The model has no network, so a contributor not materialised into /tmp/inputs/contributors.txt cannot be thanked, and an empty file means the notes say nothing about contributions at all. The two cross-file guards deleted when the changelog modal went away are back, pointing at what enforces the policy now: the marker regex in the app's parser, and the link-host allowlist in WhatsNewDrawer.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: bff5806 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🔴 Tier 4 — CriticalTouches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
Greptile SummaryThe PR rebuilds the Help menu’s “What’s new” experience from generated release notes rather than per-PR changesets.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the eligible follow-up-review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/app/scripts/parse-whats-new.js | Adds build-time parsing of release headlines, summaries, highlights, counts, versions, and changelog anchors. |
| packages/app/next.config.mjs | Generates the bounded What’s New payload while preserving runtime startup when build-only sources are unavailable. |
| packages/app/src/components/AppNav/WhatsNewDrawer.tsx | Adds the release drawer with markdown link allowlisting and image suppression. |
| packages/app/src/components/AppNav/AppNav.components.tsx | Integrates the inline release section, drawer, and unseen-release indicator into the controlled Help menu. |
| packages/app/src/components/AppNav/useWhatsNew.ts | Fetches and validates the generated release payload with deployment base-path support. |
| packages/app/src/components/AppNav/useWhatsNewUnseen.ts | Tracks whether the running application version has been acknowledged in browser storage. |
| .github/workflows/release.yml | Collects PR authorship metadata so generated release notes can acknowledge external contributors. |
| packages/app/scripts/prepare-clickhouse-build-export.js | Preserves the specifically named What’s New JSON asset without broadly retaining route-data JSON. |
| docker/hyperdx/Dockerfile | Adds the changelog parser to the application builder’s required inputs. |
| packages/app/Dockerfile | Copies the root changelog and application scripts before building the standalone app image. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Changesets[Changesets and PR metadata] --> Generator[Release notes generator]
Generator --> Changelog[Root CHANGELOG.md]
Changelog --> Parser[parse-whats-new.js]
Parser --> Payload[public/whats-new.json]
Payload --> Hook[useWhatsNew]
Hook --> Menu[Help menu summary]
Hook --> Drawer[What's new drawer]
Reviews (4): Last reviewed commit: "feat(app): mark what's actually new in t..." | Re-trigger Greptile
Deep ReviewBuild-time-generated "What's new" feed replacing the per-PR changelog modal. 12 reviewers ran across correctness, security, reliability, testing, maintainability, standards, and stack-specific lenses. The implementation is unusually careful — the react-markdown link/image sanitization boundary holds (no 🔴 P0/P1 — must fix
🟡 P2 — recommended
🔵 P3 nitpicks (7)
Reviewers (12): correctness, testing, maintainability, project-standards, security, reliability, adversarial, kieran-typescript, julik-frontend-races, previous-comments, agent-native, learnings. Testing gaps: the |
E2E Test Results✅ All tests passed • 307 passed • 1 skipped • 1332s
Tests ran across 4 shards in parallel. |
The whats-new.json contract test reached into scripts/ for the parser, which needs a no-restricted-imports exemption, and a second exemption put the app one over its eslint-disable baseline. The test moves in beside the parser tests, which already hold that exemption, rather than raising the ceiling again. The badge assertions used a plain getByText, which is a case-insensitive substring match, so 'Breaking' matched both the badge and the headline "A breaking change" and the locator resolved to two elements. Both are exact now.
Plans, specs and Playwright MCP page snapshots are local working files. A git add -A swept them into a merge commit once already.
e29bf77 to
65679f7
Compare
The "What's new" peek looked identical whether or not there was anything you hadn't read, and the nav-icon sparkle was spent the moment the menu opened. Sparkle the section label too, so the signal survives long enough to point at the thing it's about. Opening the menu marks the release seen, which clears the flag in the same tick, so the label renders off a snapshot taken in onOpen — passing the live flag down gives a sparkle that never appears. Also here: - "View all releases" is a real button rather than a dimmed anchor. Under the equally dimmed counts line it read as a third line of prose instead of the section's one action. Deliberately `secondary` and not the `link` variant code_style.md nominates, since link styling is what made it disappear in the first place. - The Breaking/New badge moves into a fixed-width column, so every headline shares one left edge instead of starting wherever its own badge ended. That row was duplicated between the menu and the drawer, which is why the ragged edge existed in both, so it's now one shared component. - Screen readers get a "New since your last visit" cue. The nav item's aria-label is gone by the time the menu is open and the glyph is decorative, so there was otherwise no signal at all.
The Help menu's "What's new" was built from the app's per-PR changesets, which have no notion of which change mattered: it keyed on a
feat:prefix, so v2.36.0 listed three dashboard-variable entries and mentioned neither formulas nor the alert work the release was actually about. It now reads the rootCHANGELOG.md— the release-level summary written and reviewed on the release PR — and the release generator writes the headline the panel needs to title each release.CleanShot.2026-08-25.at.16.45.12.mp4
What changed
The panel's content is the release notes. Each release contributes the headline and opening sentence the notes lead with, its breaking changes and new features as individual rows badged
Breaking/New(taken from the bolded lead-in of each bullet), and a count for the rest — "5 improvements, 10 bug fixes and 1 build / packaging" — linking to that release's section of the changelog. Nothing is authored in the app:whats-new-highlights.jsonand the hand-written hero it fed are gone.The generator writes a headline. Release notes now open with a one-line bolded headline. The panel needed a title per release and had no source for one, so it previously either used a hand-written card or led with a paragraph of prose.
External contributions are thanked. A bullet citing a PR opened from outside the team now reads
(#2909, thanks @alice!). The workflow already resolved each changeset commit to a PR number for the(#NNN)references; the samegh apicall now carries the author and their association with the repo.Key decisions
The root changelog, not
packages/app/CHANGELOG.md. The package changelog is one changeset per PR with no sense of significance, and its prefixes are inconsistent. The root notes are written once per release with the intent of being read.Links are pinned to the release's tag, not
main. Merging doesn't deploy, somain's changelog describes releases a given deployment isn't running. The tag comes from the release in the payload rather than the running build — mid-cycle the running version has no tag, while every release in the payload does.Improvements and bug fixes are counted, not listed. Ten fix bullets crowd out the two changes worth reading. The count preserves the fact that they exist and the link reaches the detail.
Releases predating the generated headline split their opening sentence. Those notes are written "the claim: the detail", so the claim becomes the headline and the detail the summary. Without it, every existing release leads with a paragraph.
Impact
Users see a different "What's new": headline and one sentence per release, breaking changes called out and sorted above features, counts instead of a fix list. No config, no migration.
The generated
public/whats-new.jsonis written whennext.config.mjsis evaluated, so a running dev server keeps serving the old payload until it restarts.The release notes' summary is model-authored from changeset bodies and PR titles, which anyone opening a PR can influence. The drawer renders it with images dropped and link targets allowlisted at the AST level — the same policy
release-notes.mjsgreps for in CI, with a test pinning the two together. This restores the check that went away with the changelog modal.Implementation detail
packages/app/scripts/parse-whats-new.jsis rewritten: it splits on## v<version> — <date>headings, reads the preamble for the headline and first sentence, collects bullets under Breaking Changes and New Features, and counts every other section by its heading (so a section added later counts rather than vanishing). The appended package list is excluded explicitly. A headline opening with a code span keeps its case, since capitalisingclickstack_emerging_signalsrenames it to something that doesn't exist.Docker builder stages copy the root changelog and
scripts/as build inputs instead of the package changelog and the deleted highlights file. A generation failure keys on whetherpublic/whats-new.jsonexists rather than onNEXT_PHASE, keeping the hardening #2737 added —NEXT_PHASEis an undocumented Next internal and would ship a silently empty panel if it were ever unset.The contributor filter treats
MEMBER,OWNERandCOLLABORATORas internal and excludes bots by bothuser.typeand a[bot]login suffix. The model has no network, so a contributor not materialised into/tmp/inputs/contributors.txtcan't be thanked, and an empty file means the notes say nothing about contributions.Tests: the parser's headline, summary, counts and anchor extraction;
allowChangelogUrl(lookalike hosts, port and userinfo forms, non-https, unparseable); count formatting and singularisation; a contract test running the real changelog through the parser and the runtime zod schema, which is the only thing pinning the build-time and runtime halves together. The e2e spec covers the link allowlist through react-markdown for real, since Jest stubs it out. The two cross-file guards deleted with the changelog modal are restored, pointing at the parser's marker regex and the drawer's host allowlist.