Manifest mode: raw `<word>` in a BREAKING CHANGE footer is not HTML-escaped and silently skips component releases

Open Beginner friendly
#2,952 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
73/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
node.js, typescript
Domain
release

Research direction

Start with src/changelog-notes/default.ts, where the issue identifies breaking-change note text being added without htmlEscape. Read the changelog note generation and the release PR body parsing path, then reproduce with the provided DefaultChangelogNotes and PullRequestBody example. Done means raw tag-like text in a BREAKING CHANGE footer no longer causes component release sections to be dropped.

Written by the indexing model from the issue text.

Description

priority: p3 type: bug

Related to #2801, #2899 and #2884 (and the proposed fix in #2907), but a different source of the raw tag: the text comes from a BREAKING CHANGE: footer, which htmlEscape never sees.

Environment details
  • OS: macOS 27 (also on ubuntu-latest in GitHub Actions)
  • Node.js version: v26.13.0
  • npm version: 11
  • release-please version: 17.11.2 (via googleapis/[email protected])
  • Manifest mode, node-workspace plugin, 17 packages, one grouped release PR
What happens

A commit pushed directly to the default branch had this footer:

BREAKING CHANGE: requires Biome 2.6.0 or newer. Extend presets as @kcconfigs/biome/configs/<name> instead of @kcconfigs/biome or @kcconfigs/biome/<name>.

Through the workspace plugin, that breaking-change note ended up in 11 of the 15 <details> sections of the release PR, with <name> unescaped. After the merge, release-please created 5 of 15 releases and logged Pull request contains releases, but not for component: ... for the rest. The workflow still succeeded. The manifest, package.json versions and CHANGELOGs all moved forward without tags, so the next release PR recomputed versions from older tags and proposed versions lower than ones already on npm.

The same happened on the release PR before it (2 of 15 tagged).

Real cases: https://github.com/kc-workspace/kcws/pull/285 and https://github.com/kc-workspace/kcws/pull/286

Root cause

In src/changelog-notes/default.ts, only the subject is escaped:

const notes = commit.notes
  .filter(note => note.title === 'BREAKING CHANGE')
  .map(note => replaceIssueLink(note, ...)); // no htmlEscape
let subject = htmlEscape(commit.bareMessage);

The breaking-change note text goes into the PR body raw. When the merged PR is parsed, node-html-parser opens <name> as an element, and every later <details> gets nested inside it and dropped (same mechanism as #2801).

Steps to reproduce
// npm i --ignore-scripts --save-exact [email protected]
const {DefaultChangelogNotes} = require('release-please/build/src/changelog-notes/default.js');
const {parseConventionalCommits} = require('release-please/build/src/commit.js');
const {PullRequestBody} = require('release-please/build/src/util/pull-request-body.js');
const {Version} = require('release-please/build/src/version.js');
(async () => {
  const commits = parseConventionalCommits([{
    sha: 'abc123', files: [],
    message: 'feat!: move presets\n\nBREAKING CHANGE: extend presets as pkg/configs/<name> instead.',
  }]);
  const notes = await new DefaultChangelogNotes().buildNotes(commits, {
    host: 'https://github.com', owner: 'o', repository: 'r',
    version: '2.0.0', previousTag: 'v1.0.0', currentTag: 'v2.0.0', targetBranch: 'main',
  });
  console.log(notes.split('\n').filter(l => l.includes('name')).join('\n'));
  // => * extend presets as pkg/configs/<name> instead.
  const releaseData = ['pkg-a', 'pkg-b', 'pkg-c'].map(component => ({component, version: Version.parse('2.0.0'), notes}));
  const body = new PullRequestBody(releaseData).toString();
  console.log(PullRequestBody.parse(body).releaseData.map(d => d.component));
  // => [ 'pkg-a' ]   (pkg-b and pkg-c silently dropped)
})();
Workaround

Escaping the PR body by hand (&lt;name&gt;) is not enough, because of #2899: the first parse decodes it back to <name> before the strategies parse it again. What worked was editing the release PR body right before merging to replace <name> with something that isn't tag-like (for example {name}).

Suggested fix

Run the breaking-change note text through htmlEscape too, or apply the section-marker parsing from #2907 so note content can't affect section discovery at all.

Dominant language
TypeScript
Stars
7.8k
Forks
621
Avg merge
1d 23h
Merged PRs (30d)
8

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from googleapis/release-please

All issues in googleapis/release-please

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.