Skip to content

(changelog): keep a user-facing changelog, and show What's new after an update - #365

Merged
jbr-sekoia merged 5 commits into
mainfrom
feat/changelog-whats-new
Sep 30, 2026
Merged

jbr-sekoia merged 5 commits into
mainfrom
feat/changelog-whats-new

Conversation

@jbr-sekoia

Copy link
Copy Markdown
Collaborator

Closes #363

What changes

CHANGELOG.md at the root: ## Unreleased, then ## vX.Y.Z — YYYY-MM-DD sections, newest first, each with ### New / ### Changed / ### Fixed. An entry says in one or two sentences what the user sees, with its ref at the end. A --- line closes the last section, and the pointer to GitHub Releases for older versions sits under it.

The rule, where it is read: docs/changelog.md covers the file, how to write an entry, the CI check and the dialog. It is linked from docs/releasing.md, docs/README.md and CHANGELOG.md itself. .ai/shared-guidelines.md carries one new "When you finish work" step and one orientation row.

Release procedure (docs/releasing.md, .claude/commands/release.md): the bump PR renames ## Unreleased to the version and its date, and opens a new empty one. The publish job's "Fill in release notes" step now writes the body from that section (scripts/changelog-section.js <tag>), followed by the same Full changelog compare link as before. It still fails rather than publish an empty body, now when the section is missing or empty. The commit-subject list is gone. Step 9 of the command no longer prepends a summary: the notes and the app's dialog must say the same thing, so wording is changed in CHANGELOG.md. test/changelog.test.js fails when package.json's version has no section, so a bump without one does not pass CI.

CI check: a changelog job in test.yml, on pull requests only.

  • It fails when the PR's diff (HEAD^1..HEAD of the merge commit) touches app code but not CHANGELOG.md.
  • App code is what electron-builder ships: the root *.js files except eslint.config.js, public/, workers/ and scripts/claude-sandbox.sh.
  • The no-changelog label (created) waives it. The labels are read through the API at run time, so after adding the label a re-run of the failed job passes. labeled events do not re-run the whole test matrix.
  • The logic is scripts/check-changelog.js.

What's new dialog:

  • Main side (changelog.js, main.js): whats-new-startup compares app.getVersion() with global.lastSeenVersion, whose default null is in SETTING_DEFAULTS.
    • No valid stored version (a fresh install): the running version is recorded and no dialog opens.
    • Same or later version stored: no dialog.
    • Earlier version stored: the dialog opens with every section in (lastSeen, current], newest first, skipped versions included. whats-new-dismissed records the running version when it closes.
    • Missing or unparsable file: [whats-new] warning in the main log, no dialog, nothing recorded.
    • Versions compare numerically: 0.0.100 > 0.0.99.
  • CHANGELOG.md is added to build.files and read from __dirname, which is app.asar in a packaged build.
  • Help → What's new is a new last menu (window-frame.js). It sends show-whats-new with the running version's section.
  • Renderer (public/whats-new.js):
    • A parser for the subset the file uses: ###, - bullets, paragraphs, bold, inline code, links.
    • Every piece of text goes through escapeHtml. Only an http(s) URL with no quote or angle bracket becomes a link, and a click on it goes to openExternal.
    • Built like the Add Project dialog, with the sidebar's thin scrollbar. It closes on ×, on Escape (caught in the capture phase so a terminal behind it does not get it) and on a backdrop click.
    • .whats-new-overlay is in the window strip's no-drag list.

Evidence

Tests written first, each seen red on its assertions against a stub, then green:

File Tests
test/changelog.test.js 23
test/whats-new.test.js 23
test/check-changelog.test.js 10
test/changelog-section.test.js 5
test/whats-new-wiring.test.js 6
test/window-frame.test.js +1, and one regex updated

Mutations: each guard was mutated alone and the suite rerun. All 66 are caught.

Area Caught
Parser and decisions 20/20
CI check 13/13
Renderer (escaping, link scheme, quote breakout, stacking, Escape propagation, focus, CSS scroll and no-drag) 22/22
Menu and main wiring, packaging 8/8

The first pass had four survivors:

  • The non-string guard in parseChangelog: split throws anyway, so the guard is removed.
  • Three tests that could not see their bug (a quote-breakout input the regex never matched, unescaped section headings, a blank line between paragraphs). Those tests were strengthened.

Live (Playwright, throwaway HOME, one data directory across three launches of this branch):

  1. Fresh start: no dialog, lastSeenVersion = 0.0.84.
  2. lastSeenVersion seeded to 0.0.81, then relaunched:
    • The dialog lists v0.0.84, v0.0.83, v0.0.82.
    • The body scrolls (overflow-y: auto, scrollHeight 665 > 663) and the dialog stays under 80 vh.
    • The close button has focus, and nothing is recorded before dismissal.
    • With prefers-color-scheme: light emulated, the dialog keeps its colours.
    • Escape closes it and records 0.0.84.
    • Help → What's new, clicked through the real application menu, shows v0.0.84 alone; × closes it.
  3. Relaunch: no dialog.

15/15 checks passed.

Gate: eslint . has 0 errors, and its 343 warnings are all pre-existing. The full suite runs through the pre-commit hook.

…an update

Nothing told a user what changed when the app updated: the GitHub release
notes were the commit subjects, written about the code, and nobody opens
them after an automatic update.

- CHANGELOG.md at the root: an Unreleased section, then one section per
  version with New / Changed / Fixed entries on what the user sees.
  Backfilled for v0.0.80 to v0.0.84 and filled with what main carries
  since v0.0.84.
- docs/changelog.md holds the writing rule; the finish-work list in
  .ai/shared-guidelines.md asks every behaviour change for its entry.
- The release bump dates the Unreleased section, and the publish job
  writes the release body from that section (scripts/changelog-section.js)
  instead of the commit subjects, still failing on an empty body.
- A changelog job in test.yml fails a PR that changes shipped app code
  without touching CHANGELOG.md, unless it has the no-changelog label.
- A What's new dialog: on the first start after an update it lists every
  section after the stored lastSeenVersion up to the running version,
  skipped versions included, and records the version when closed. A fresh
  install records the version and shows nothing; a missing or unparsable
  file shows nothing and logs a warning. Help -> What's new reopens the
  running version's section. CHANGELOG.md is packaged and read main-side.

Closes #363
…e CI check end to end

- An install updated from v0.0.84 has no lastSeenVersion, so it took the
  fresh-install path and never saw the dialog. main.js now records, as it
  loads, whether the database already held a global settings row or the
  initial-scan marker; such an install without the key is treated as having
  seen 0.0.84, the last release without the dialog.
- The startup, dismissal and menu logic moves into createWhatsNew() in
  changelog.js, so dismissing is tested against a settings store: the
  version persists, other global keys survive, and the next start shows
  nothing. Deleting the write no longer leaves the suite green.
- scripts/check-changelog.js takes the PR's base and head SHAs and diffs
  them through their merge base, so every commit of the PR counts and
  changes from main do not. It reads the labels through gh itself; the
  workflow only invokes it. Tested end to end on a temporary repository
  and a fake gh.
- The build workflow's checkout comment says what full history is still
  for: finding the previous tag.
Brings in #355, whose entry goes under Unreleased next.
What a user sees of #355: a save no longer overwrites a file that changed
on disk, an external change is reloaded or reported, file tabs keep their
unsaved edits across tab and session switches, and undo stays within the
file on screen. Its Windows work-file index fix has no entry: the index
signature carries each file's mtime and size, so a save was reindexed on
the next listing anyway.
…dd two #355 entries

- The CHANGELOG check diffs with --no-renames: with rename detection,
  moving public/foo.js to docs/foo.js listed only docs/foo.js, and the PR
  passed without an entry.
- The e2e tests create their repositories with git init and
  symbolic-ref instead of git init -b, which git 2.24 does not know.
- Unreleased gains two #355 fixes users see: a failed save now says so,
  and an answered MCP diff's Save is disabled.
@jbr-sekoia
jbr-sekoia merged commit 5756d47 into main Sep 30, 2026
11 checks passed
@jbr-sekoia
jbr-sekoia deleted the feat/changelog-whats-new branch September 30, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(changelog): a user-facing changelog, and a What's new dialog after an update

1 participant