diff --git a/src/util/pull-request-body.ts b/src/util/pull-request-body.ts index bf16e3ac4..e368ce027 100644 --- a/src/util/pull-request-body.ts +++ b/src/util/pull-request-body.ts @@ -126,7 +126,17 @@ function extractMultipleReleases(notes: string, logger: Logger): ReleaseData[] { const root = parse(notes); for (const detail of root.getElementsByTagName('details')) { const summaryNode = detail.getElementsByTagName('summary')[0]; - const summary = summaryNode?.textContent; + if (!summaryNode) { + // A commit subject can legitimately contain a token like + // `
` inside an inline code span. The HTML parser does not + // understand markdown, so it sees that text as a real element. + // Skip it and let the caller fall back to single-release parsing. + logger.warn( + 'found a
block without a , skipping it - the release notes may contain raw HTML from commit subjects' + ); + continue; + } + const summary = summaryNode.textContent; const match = summary.match(SUMMARY_PATTERN); if (match?.groups) { detail.removeChild(summaryNode); diff --git a/test/util/pull-request-body.ts b/test/util/pull-request-body.ts index 1f1e92309..63984a908 100644 --- a/test/util/pull-request-body.ts +++ b/test/util/pull-request-body.ts @@ -135,6 +135,50 @@ describe('PullRequestBody', () => { expect(releaseData[0].version?.toString()).to.eql('0.1.0'); expect(releaseData[0].notes).matches(/initial generation/); }); + + // A commit subject can contain a token like `
` inside an + // inline code span. The HTML parser does not understand markdown, so + // the release notes end up containing what looks like a real + // `
` element without a ``. + // https://github.com/googleapis/release-please/issues/2801 + it('should tolerate a
element missing a ', () => { + const body = [ + ':robot: I have created a release *beep* *boop*', + '---', + '', + '', + '## [1.2.3](https://github.com/googleapis/release-please/compare/v1.2.2...v1.2.3) (2026-08-22)', + '', + '', + '### Bug Fixes', + '', + '* escape an unbalanced `
` tag instead of refusing the draft ([#133](https://github.com/googleapis/release-please/issues/133))', + '', + '---', + 'This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).', + ].join('\n'); + const pullRequestBody = PullRequestBody.parse(body); + expect(pullRequestBody).to.not.be.undefined; + const releaseData = pullRequestBody!.releaseData; + expect(releaseData).lengthOf(1); + expect(releaseData[0].version?.toString()).to.eql('1.2.3'); + expect(releaseData[0].notes).matches(/unbalanced/); + }); + + it('should keep parsing valid components around a
element missing a ', () => { + const body = readFileSync( + resolve(fixturesPath, './multiple.txt'), + 'utf8' + ); + // Inject a stray, summary-less
block before the valid ones. + const poisoned = body.replace( + '
', + '
\nchore: mention `
` handling\n
\n
' + ); + const pullRequestBody = PullRequestBody.parse(poisoned); + expect(pullRequestBody).to.not.be.undefined; + expect(pullRequestBody!.releaseData.length).to.be.greaterThan(0); + }); }); describe('toString', () => { it('can handle multiple entries', () => {