Skip to content

feat: build "What's new" from the generated release notes - #2993

Open
jordan-simonovski wants to merge 7 commits into
mainfrom
jordansimonovski/whatsnew-improvements
Open

feat: build "What's new" from the generated release notes#2993
jordan-simonovski wants to merge 7 commits into
mainfrom
jordansimonovski/whatsnew-improvements

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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 root CHANGELOG.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.json and 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 same gh api call 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, so main'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.json is written when next.config.mjs is 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.mjs greps 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.js is 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 capitalising clickstack_emerging_signals renames 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 whether public/whats-new.json exists rather than on NEXT_PHASE, keeping the hardening #2737 added — NEXT_PHASE is an undocumented Next internal and would ship a silently empty panel if it were ever unset.

The contributor filter treats MEMBER, OWNER and COLLABORATOR as internal and excludes bots by both user.type and a [bot] login suffix. The model has no network, so a contributor not materialised into /tmp/inputs/contributors.txt can'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.

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.
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 25, 2026 7:12am
hyperdx-storybook Ready Ready Preview Aug 25, 2026 7:12am

Request Review

@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bff5806

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

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

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches 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:

  • Background tasks or delivery pipeline substantially modified — 51 lines (bar: 30):
    • .github/workflows/release.yml
    • docker/hyperdx/Dockerfile

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 17
  • Production lines changed: 1252 (+ 942 in test files, excluded from tier calculation)
  • Critical-path lines changed: 51
  • Branch: jordansimonovski/whatsnew-improvements
  • Author: jordan-simonovski

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 25, 2026
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR rebuilds the Help menu’s “What’s new” experience from generated release notes rather than per-PR changesets.

  • Generates a bounded whats-new.json payload from the root changelog during the application build.
  • Adds release summaries, highlighted breaking changes and features, aggregate change counts, a drawer, and an unseen-release indicator.
  • Updates release-note generation to add headlines and acknowledge external contributors.
  • Adjusts Docker and ClickStack export inputs so the generated payload ships in supported deployment modes.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains in the eligible follow-up-review scope.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (4): Last reviewed commit: "feat(app): mark what's actually new in t..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Build-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 rehype-raw, disallowedElements + urlTransform cover every markdown vector), the next start re-eval fallback is sound, and the shell/jq contributor pipeline resists injection. One CI-blocking issue and two substantive test-coverage gaps stand.

🔴 P0/P1 — must fix

  • scripts/ci/ratchet-baseline.json:10 — the app.eslint-disable baseline is raised to 146, but the branch adds four new eslint-disable comments (in WhatsNewDrawer.tsx, WhatsNewSection.tsx, useWhatsNewUnseen.ts, and __tests__/parse-whats-new.test.ts) against zero in the deleted ChangelogModal files, so the actual count is 148; node scripts/ci/ratchet.mjs fails with 148 > baseline 146. Independently confirmed by re-counting the working tree.
    • Fix: set app.eslint-disable to 148, or remove two of the four new disables.

🟡 P2 — recommended

  • .github/workflows/release.yml:402 — the new contributor-classification logic (author_association MEMBER/OWNER/COLLABORATOR skip, user.type == Bot skip, *[bot] login skip, TSV cut parsing) lives as untested inline shell, so a regression that thanks a bot, thanks a maintainer, or drops a real outside contributor ships silently into user-facing release notes.
    • Fix: extract the per-record contributor decision into a release-notes.mjs subcommand and cover its branches under the existing node --test corpus.
    • maintainability, testing, reliability, correctness
  • packages/app/next.config.mjs:57 — the build-time generation and its throw-vs-warn fallback (dynamic import, existsSync(whats-new.json) guard) have no test; a regression either fails every build or ships an empty/absent asset that renders an empty panel for every user, and only the parser output — not this branch — is currently exercised.
    • Fix: extract the generate-and-guard step into a testable function and unit-test both the missing-asset-throws and stale-asset-warns paths.
    • testing, reliability
🔵 P3 nitpicks (7)
  • packages/app/scripts/parse-whats-new.js:47firstSentence truncates a summary or bullet at an abbreviation not in the four-entry list, so "Runs at 9 a.m. every day and refreshes." is cut to "Runs at 9 a.m".
    • Fix: broaden the abbreviation set (a.m., p.m., U.S.) or require the terminating period to be preceded by more than one character.
  • packages/app/scripts/parse-whats-new.js:91 — a section heading with no letters (emoji-only) yields an empty count label and empty anchor, rendering "5 " with no noun and a bare # deep-link.
    • Fix: fall back to the raw trimmed heading, or skip the section, when the stripped label is empty.
  • packages/app/scripts/parse-whats-new.js:241headingRe/toSections/toItems have no code-fence awareness, so a fenced block containing a ## line is parsed as a spurious release and can push a real release out of the maxReleases window.
    • Fix: strip fenced code blocks before matching structural lines.
  • packages/app/src/components/AppNav/WhatsNewSection.tsx:37 — the menu's onOpen calls markWhatsNewSeen() unconditionally, but the section destructures only data and has no error/loading branch, so on a slow or failed fetch the sparkle is permanently cleared while the peek shows an empty timeline.
    • Fix: defer markWhatsNewSeen() until the query resolves successfully, and add isError/isLoading handling mirroring the drawer.
  • packages/app/src/components/AppNav/useWhatsNew.ts:113 — the whats-new.json fetch has no timeout, so a hung connection leaves an indefinite spinner instead of routing to the existing "Unable to load" branch.
    • Fix: pass AbortSignal.timeout(...) to fetch so a stalled request falls through to the error state.
  • .github/scripts/__tests__/release-notes.test.mjs:554 — the cross-file pins scrape WhatsNewDrawer.tsx and parse-whats-new.js as raw source text, so a benign reformat (splitting the Set across lines, prettier reflow) fails a release-notes test with a confusing cross-package error.
    • Fix: export the shared host list and marker from a module both sides import, or add a comment at each scraped literal noting the exact syntax is asserted by a CI test elsewhere.
    • testing, maintainability
  • packages/app/src/components/AppNav/WhatsNewSection.tsx:51 — the breaking-first ordering plus .slice(0, PEEK_LIMIT) truncation is untested; every fixture supplies ≤2 highlights, so the case where more than three breaking changes squeeze out features is never asserted.
    • Fix: add a fixture with 4 breaking + 2 feature highlights and assert exactly 3 rows, all breaking.
    • testing, kieran-typescript

Reviewers (12): correctness, testing, maintainability, project-standards, security, reliability, adversarial, kieran-typescript, julik-frontend-races, previous-comments, agent-native, learnings.

Testing gaps: the release.yml shell pipeline is untested for a PR with a null .user (deleted account), where the jq @tsv array errors and drops the entire record — losing the (#NNN) citation, not just the thanks; no test confirms changelogUrl's %2F-encoded package-tag ref actually resolves on GitHub rather than 404-ing every drawer deep-link; no test asserts raw HTML in a summary renders inert (guarding against a future rehype-raw addition).

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 307 passed • 1 skipped • 1332s

Status Count
✅ Passed 307
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

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.
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/whatsnew-improvements branch from e29bf77 to 65679f7 Compare August 25, 2026 05:11
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant