Skip to content

fix(release): never publish an empty changelog (commit-based fallback) - #1766

Merged
lidge-jun merged 5 commits into
devfrom
fix/release-notes-commit-fallback
Aug 15, 2026
Merged

fix(release): never publish an empty changelog (commit-based fallback)#1766
lidge-jun merged 5 commits into
devfrom
fix/release-notes-commit-fallback

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Release bodies for v2.18.2 and v2.19.0 shipped as a 169-character stub - just the npm line and a compare link. Root cause: the releases/generate-notes API aggregates MERGED PULL REQUESTS over the compared tag range, and recent ranges have almost none, because work lands as direct commits on dev (or through PRs based on dev rather than the release branch). Measured on the live repo: v2.17.0..v2.18.2 = 0 of 36 commits PR-associated, v2.18.2..v2.19.0 = 2 of 21.

This adds a commit-based fallback: when the PR delta yields no categories, the workflow renders the commit log instead, categorized by conventional-commit prefix, excluding merge and release-bump commits. Replaying the real v2.18.2..v2.19.0 range turns the 169-char stub into a 1.8 KB categorized changelog.

Hardening folded in from three adversarial review rounds:

  • The commit log is NUL-framed (git log -z); Git forbids NUL in commit content, while U+001F is legal in both subjects and author names and could otherwise forge a field boundary.
  • Commit text is sanitized: the git author field is a free-form display name, so a contributor named e.g. "Abhishek Sharma" previously rendered as a live at-mention. Authors now render as plain text in a (sha, Name) trailer, and Markdown metacharacters are backslash-escaped rather than deleted so technical text stays readable.
  • Fallback generation depends on this range's PR delta only. Gating on carried notes too would silently drop every post-preview direct commit.
  • Carried commit bullets survive the preview to stable carry, and carried plus current sections merge by category so a shared heading is not emitted twice.

Verification

  • bun test tests/release-notes.test.ts: 70 pass / 0 fail (16 new regression tests).
  • Remote full gates on ssh lidge @ cfee2d2: 12371 pass / 11 skip / 0 fail (12382 tests, 786 files); typecheck, privacy:scan, lint:gui green.
  • End-to-end replay against the real empty range reproduces the failure and the fix.

Checklist

  • Targets dev
  • Release-automation change - flagged for maintainer security review per MAINTAINERS.md
  • Regression tests included

Summary by CodeRabbit

  • New Features
    • Release notes now include eligible commit-based entries when no meaningful pull request categories are available.
    • Commit entries are organized into familiar release categories and merged with carried-forward notes.
  • Improvements
    • Duplicate entries are removed, release-plumbing changes are excluded, and commit text is cleaned for readability.
    • Pull request release notes continue to take precedence when available.

…es finds no PRs

releases/generate-notes aggregates merged pull requests over the compared tag range. Work landing as direct commits on dev (or through PRs based on dev rather than the release branch) leaves that range with nothing to aggregate, so the body collapsed to the npm line plus a compare link: v2.17.0..v2.18.2 had 0 of 36 commits PR-associated and v2.18.2..v2.19.0 had 2 of 21, and both releases shipped a 169-char stub.

The workflow now detects an empty PR delta and renders the commit log instead, categorized by conventional-commit prefix, excluding merge and release-bump commits. The renderer keeps only PR-numbered entries, so the fallback travels on its own --commit-fallback channel and is emitted only when the PR pipeline produced no sections at all.
…adata

Audit findings on the first commit: (1) a preview whose notes came from the fallback would carry into a stable release as meaningful, but the PR renderer discards non-PR bullets, collapsing the stable body back to the stub; (2) %an is a free-form git display name, so a contributor named e.g. Abhishek Sharma rendered as a live @abhishek mention, and subjects could inject markdown or a forged unit separator; (3) real merges using the merge: conventional prefix were not treated as plumbing.

extractCommitBulletSections carries PR-free bullets through the renderer, sanitizeCommitText neutralizes mentions/markdown/separators, authors render as plain text in a (sha, Name) trailer, non-hex shas are dropped, parseCommitLog splits the author from the right, and merge: joins the plumbing filter. Nine regression tests cover each.
… carried notes

Second audit round: (1) U+001F is legal in git author names too, so right-splitting still let author metadata shift the subject; the log is now NUL-delimited (git log -z), which git forbids inside commit content, and records parse as exactly three fields. (2) The workflow gated fallback generation on carried notes as well as the delta, so a stable release carrying commit-based preview notes silently dropped every post-preview direct commit; the guard now depends on this range delta only and the renderer decides whether to emit. (3) The sanitizer escaped by deletion, mangling technical text like Map<K, V> | CLI; metacharacters are now backslash-escaped instead.
…ests

Third audit round: concatenating carried and current fallback bodies emitted a duplicate heading when both touched the same category; mergeCommitBulletSections now merges by category with bullet de-duplication. Two converted tests still carried literal \\u001f fixtures, so parseCommitLog returned nothing and they asserted on an empty string; both now use real NUL fields and assert the entry renders.
@lidge-jun
lidge-jun merged commit c71c827 into dev Aug 15, 2026
6 of 7 checks passed
@lidge-jun
lidge-jun deleted the fix/release-notes-commit-fallback branch August 15, 2026 10:48
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 53e0e73c-3fbe-4615-af23-329a3b3341a8

📥 Commits

Reviewing files that changed from the base of the PR and between b1b05b8 and cfee2d2.

📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • scripts/release-notes.ts
  • tests/release-notes.test.ts

📝 Walkthrough

Walkthrough

The release process now generates commit-based fallback notes when generated PR notes lack meaningful categories. The CLI parses and sanitizes eligible commits, renders categorized sections, merges them with carried notes, and integrates the result into the release workflow.

Changes

Release note fallback

Layer / File(s) Summary
Commit parsing and fallback rendering
scripts/release-notes.ts, tests/release-notes.test.ts
Commit logs are parsed from NUL-delimited fields. Release-plumbing and invalid commits are excluded. Conventional commit types map to categories, and commit text is sanitized before rendering. Tests cover parsing, filtering, categorization, sanitization, and deduplication.
Renderer and CLI integration
scripts/release-notes.ts, tests/release-notes.test.ts
renderReleaseNotes accepts optional fallback notes and uses them when PR categories are absent. The commit-fallback command reads, renders, and writes fallback notes. Tests cover carry-forward handling, precedence, empty output, and complete rendering.
Release workflow fallback wiring
.github/workflows/release.yml
The workflow initializes a fallback file, creates notes from the selected commit range when PR categories are absent, and passes the file to the release-note renderer.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseWorkflow
  participant Git
  participant CommitFallbackCLI
  participant ReleaseNotesRenderer
  ReleaseWorkflow->>Git: Read commits from notes_range_start..GITHUB_SHA
  Git-->>ReleaseWorkflow: NUL-delimited commit log
  ReleaseWorkflow->>CommitFallbackCLI: Render commit-fallback notes
  CommitFallbackCLI-->>ReleaseWorkflow: Categorized fallback notes
  ReleaseWorkflow->>ReleaseNotesRenderer: Render carried, generated, and fallback notes
  ReleaseNotesRenderer-->>ReleaseWorkflow: Complete release body
Loading

Possibly related PRs

Suggested reviewers: wibias, ingwannu

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/release-notes-commit-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfee2d2c9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/release-notes.ts
Comment on lines +721 to +722
const renderedAnyPrSection = parts.length > (npmMetadata ? 1 : 0);
if (!renderedAnyPrSection) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fallback commits when carried previews contain PRs

When a stable release carries any preview PR section but its post-preview range contains only direct commits, the Create GitHub release workflow deliberately populates commitFallbackNotes; however, the carried PR makes renderedAnyPrSection true, so this branch discards the current fallback and any carried commit-only preview bullets. The resulting stable notes silently omit all direct changes in that delta. Merge the commit bullets into the existing category output, or only let PR content from the same delta suppress its fallback.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant