fix(#3001): indent continuation lines in serializeChangelog (#3101)

* fix(#3001): indent continuation lines in serializeChangelog

serializeChangelog interpolated bullet bodies verbatim into a single
- ${body} (#${pr}) line. Any embedded newline became a column-0 line;
parseChangelog's continuation-fold (/^\s+/) didn't pick it up, so
flushBullet terminated the bullet early — dropping the continuation text
and the (#NNNN) PR trailer (recorded as pr: null). The round-trip property
serialize(IR) → parse(text) === IR was false for any body containing \n.

Fix: indent continuation lines (body.replace(/\n/g, '\n  ')) so the
parser folds them correctly. Round-trip test asserts both paragraphs'
content AND the PR number survive.

* chore(#3001): backfill changeset PR number 3101

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-05 22:22:30 -04:00
committed by GitHub
parent f49b9b8f70
commit 2f5b6a9b48
3 changed files with 39 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3101
---
**Multi-paragraph changeset bodies no longer truncate and lose their PR trailer** — `serializeChangelog` wrote bullet bodies verbatim, so an embedded newline became a column-0 line that `parseChangelog` treated as the end of the bullet, silently dropping the continuation and the `(#NNNN)` trailer. Continuation lines are now indented so the round-trip preserves content and attribution. (#3001)

View File

@@ -26,7 +26,11 @@ function serializeChangelog(ir) {
lines.push(`### ${section.type}`);
lines.push('');
for (const b of section.bullets) {
lines.push(`- ${b.body} (#${b.pr})`);
// #3001: indent continuation lines so parseChangelog's continuation-fold
// (/^\s+/) picks them up instead of terminating the bullet at the first
// blank line. A multi-paragraph body round-trips with content preserved.
const body = b.body.replace(/\n/g, '\n ');
lines.push(`- ${body} (#${b.pr})`);
}
lines.push('');
}

View File

@@ -152,3 +152,32 @@ describe('changeset serialize: multi-section + prior content (#2975)', () => {
assert.equal(back.releases[1].sections[0].bullets[0].pr, 100);
});
});
// ─── #3001: multi-paragraph body round-trip ────────────────────────────────
describe('#3001: multi-paragraph changeset body round-trips without truncation', () => {
test('a body containing \\n\\n retains content from both paragraphs AND its PR number', () => {
const ir = {
releaseHeader: { version: '1.42.0', date: '2026-08-05' },
sections: [{
type: 'Fixed',
bullets: [{
pr: 2595,
body: 'First paragraph, load-bearing.\n\nSecond paragraph, also load-bearing.',
}],
}],
priorChangelog: null,
};
const text = serializeChangelog(ir);
const back = parseChangelog(text);
const bullet = back.releases[0].sections[0].bullets[0];
// PR trailer must survive (was lost pre-fix because the trailer was on the dropped side).
assert.equal(bullet.pr, 2595,
`PR number must survive round-trip; got: ${bullet.pr}`);
// Content from BOTH paragraphs must be present (was truncated pre-fix).
assert.ok(bullet.body.includes('First paragraph'),
`first paragraph must survive; got: ${JSON.stringify(bullet.body)}`);
assert.ok(bullet.body.includes('Second paragraph'),
`second paragraph must survive; got: ${JSON.stringify(bullet.body)}`);
});
});