From 5582644dc49330549be2a3a4cdd5b1ba0f21a294 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:39:21 +0200 Subject: [PATCH] fix(whatsnew): decode HTML entities and trim em-dash detail in fallback bullets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Features-fallback showed raw changelog text, so a commit subject like 'branded URL shortener — /s/ with OG injection' surfaced two problems in the admin banner: - release-please escapes to <slug>; React renders the literal entity, so the banner read '/s/<slug>'. Decode the entities (< > & " '), & last to avoid double-decoding. - the technical tail leaked into a user-facing highlight. Drop a trailing '— detail' clause (em dash only, so 'mark-paid' is untouched) so the bullet reads as the headline 'branded URL shortener'. Only affects the deterministic fallback; curated blocks are unchanged. --- backend/__tests__/utils/whatsNew.test.js | 15 ++++++++++++++ backend/src/utils/whatsNew.js | 25 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/__tests__/utils/whatsNew.test.js b/backend/__tests__/utils/whatsNew.test.js index 6e91a61a..d396059d 100644 --- a/backend/__tests__/utils/whatsNew.test.js +++ b/backend/__tests__/utils/whatsNew.test.js @@ -33,6 +33,21 @@ describe('parseWhatsNew', () => { ]); }); + it('decodes HTML entities release-please escapes into changelog text', () => { + const body = '### Features\n* **gallery:** supports A & B <tags> "quoted" ([#1](http://x))'; + expect(parseWhatsNew(body)).toEqual(['supports A & B "quoted"']); + }); + + it('trims a trailing "— implementation detail" clause to the headline', () => { + const body = '### Features\n* **gallery:** branded URL shortener — /s/<slug> with OG injection ([#699](http://x))'; + expect(parseWhatsNew(body)).toEqual(['branded URL shortener']); + }); + + it('leaves hyphenated words and dash-free bullets intact', () => { + const body = '### Features\n* **invoices:** mark-paid now supports bank transfer ([#2](http://x))'; + expect(parseWhatsNew(body)).toEqual(['mark-paid now supports bank transfer']); + }); + it('excludes Bug Fixes from the fallback', () => { const body = '### Features\n* **a:** feature one\n### Bug Fixes\n* **b:** fix one'; expect(parseWhatsNew(body)).toEqual(['feature one']); diff --git a/backend/src/utils/whatsNew.js b/backend/src/utils/whatsNew.js index cf16b34e..796ba386 100644 --- a/backend/src/utils/whatsNew.js +++ b/backend/src/utils/whatsNew.js @@ -14,14 +14,35 @@ const MAX_BULLETS = 8; +/** + * Decode the handful of HTML entities release-please escapes into changelog + * text (a raw "/s/" in a commit subject lands as "/s/<slug>"). + * Without this the banner shows the literal entity, since React renders text + * nodes verbatim. `&` is decoded last so "&lt;" stays "<". + */ +function decodeEntities(s) { + return s + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/�*39;|�*27;|'/gi, "'") + .replace(/&/g, '&'); +} + /** Strip list markers, conventional-commit scope, and trailing PR/sha links. */ function cleanBullet(line) { - return line + return decodeEntities(line .replace(/^\s*[-*]\s+/, '') // "- " / "* " marker .replace(/^\*\*([^:*]+):\*\*\s*/, '') // "**scope:** " prefix .replace(/\s*\(\[[^\]]*\]\([^)]*\)\)/g, '') // " ([#41](url))" / " ([sha](url))" .replace(/\s*\(#\d+\)/g, '') // bare " (#41)" - .replace(/`/g, '') + .replace(/`/g, '')) + // Drop a trailing "— implementation detail" clause so a release highlight + // reads as the headline ("branded URL shortener"), not the commit subject + // ("branded URL shortener — /s/ with OG injection"). Em dash only, so + // hyphenated words ("mark-paid") are untouched. Skipped if it would empty + // the bullet (i.e. nothing before the dash). + .replace(/^(.+?\S)\s+—\s+.*$/, '$1') .replace(/\s+/g, ' ') .trim(); }