From 89a231f3e0a8bc708c6603312fe51355437d8505 Mon Sep 17 00:00:00 2001 From: Sauer Date: Mon, 9 Nov 2026 22:12:55 +0100 Subject: [PATCH] Keep wrapped changelog bullets together in release notes Indented continuation lines belonged to a bullet in the changelog but rendered as a separate paragraph in Settings > Updates. The release workflow now puts the changelog section before the image list, since that body is what admins read in the app. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 5 +++-- .../components/settings/updates-section.test.tsx | 10 +++++++++- src/components/settings/release-notes.tsx | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9231924..4e3970c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -256,7 +256,10 @@ jobs: exit 1 fi + # Changelog first: Settings > Updates shows this body to admins. { + cat "$CHANGELOG_SECTION" + echo echo "## Docker images" echo echo "- \`ghcr.io/$OWNER/stowage-app:$VERSION\`" @@ -267,8 +270,6 @@ jobs: echo "## Deployment bundle" echo echo "- \`docker-compose.release.yml\`" - echo - cat "$CHANGELOG_SECTION" } > release-notes.md - name: Create GitHub Release diff --git a/src/__tests__/components/settings/updates-section.test.tsx b/src/__tests__/components/settings/updates-section.test.tsx index 43936ac..1ef49ef 100644 --- a/src/__tests__/components/settings/updates-section.test.tsx +++ b/src/__tests__/components/settings/updates-section.test.tsx @@ -33,7 +33,8 @@ const release = { name: "Stowage 0.3.0", publishedAt: Date.parse("2026-11-02T09:00:00Z"), url: "https://github.com/kroqdotdev/stowage/releases/tag/v0.3.0", - notes: "## Added\n\n- In-app updates\n- A `thing`", + notes: + "## Added\n\n- In-app updates\n- A `thing` that wraps onto\n the next line\n- Last", prerelease: false, }; @@ -98,6 +99,13 @@ describe("UpdatesSection", () => { ).toHaveAttribute("href", release.url); expect(within(list).getByText("In-app updates")).toBeInTheDocument(); expect(within(list).getByText("thing")).toBeInTheDocument(); + // The wrapped bullet stays one item; its continuation is not a paragraph. + const items = within(list).getAllByRole("listitem"); + const wrapped = items.find((item) => + item.textContent?.includes("that wraps onto the next line"), + ); + expect(wrapped?.tagName).toBe("LI"); + expect(within(list).queryByText(/^the next line$/)).not.toBeInTheDocument(); expect(screen.getByText("Update by hand")).toBeInTheDocument(); expect( diff --git a/src/components/settings/release-notes.tsx b/src/components/settings/release-notes.tsx index 04b290c..d042537 100644 --- a/src/components/settings/release-notes.tsx +++ b/src/components/settings/release-notes.tsx @@ -62,7 +62,11 @@ export function ReleaseNotes({ const line = raw.trimEnd(); const heading = /^(#{1,6})\s+(.*)$/.exec(line); const bullet = /^\s*[-*]\s+(.*)$/.exec(line); - if (heading) { + // A wrapped bullet continues on indented lines in the changelog. + const continuation = /^\s+\S/.test(line) && bullets.length && !bullet; + if (continuation) { + bullets[bullets.length - 1] += ` ${line.trim()}`; + } else if (heading) { flushBullets(); flushParagraph(); const title = heading[2].replace(/^\[([^\]]+)\].*$/, "$1");