Skip to content

fix(generate): place release changelog entries deterministically#129

Merged
jdx merged 2 commits intomainfrom
codex/fix-changelog-release-order
Apr 26, 2026
Merged

fix(generate): place release changelog entries deterministically#129
jdx merged 2 commits intomainfrom
codex/fix-changelog-release-order

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 26, 2026

Summary

  • stop asking the LLM to rewrite and place tagged release changelog sections
  • deterministically replace an existing version section or insert the new one immediately after [Unreleased]
  • error when an existing changelog is missing the [Unreleased] anchor instead of guessing a placement
  • keep matching the existing version header style while using the generated changelog entry text

Validation

  • cargo fmt --check
  • cargo test generate
  • cargo clippy -- -D warnings

Note

Medium Risk
Replaces an LLM-driven CHANGELOG.md rewrite with deterministic parsing/insertion logic; risk is moderate because it changes how release sections are located/removed/inserted and could mis-handle unusual changelog formats.

Overview
Stops using the LLM to rewrite CHANGELOG.md for tagged releases, and instead deterministically upserts the target version section: remove any existing section for that version and insert the new one immediately after ## [Unreleased]/## Unreleased.

Adds header-style inference via format_version_header (preserving bracket/link/date conventions), plus new helpers to remove/reinsert sections and to validate/repair changelog structure (including erroring on missing or duplicate Unreleased sections). Expands tests to cover ordering, validation in --dry-run, and header formatting.

Reviewed by Cursor Bugbot for commit 406418c. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 94.96403% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.70%. Comparing base (f24c71d) to head (406418c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/generate.rs 94.96% 6 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #129      +/-   ##
==========================================
- Coverage   94.86%   94.70%   -0.17%     
==========================================
  Files          26       26              
  Lines        4888     5004     +116     
  Branches     4888     5004     +116     
==========================================
+ Hits         4637     4739     +102     
- Misses        159      173      +14     
  Partials       92       92              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the changelog update process by replacing the LLM-based editing logic with a deterministic local implementation. It introduces upsert_version_changelog and associated helper functions to handle version header formatting and section placement, specifically ensuring new versions are inserted after the [Unreleased] section. Feedback focuses on improving the robustness of the insertion logic when the [Unreleased] anchor is missing and ensuring that the upsert calculation is performed during dry runs to catch potential errors early.

Comment thread src/generate.rs
Comment on lines +2135 to +2137
let Some(unreleased) = find_unreleased_section(&without_existing)? else {
return Ok(join_changelog_head_tail(&section, &without_existing));
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If the [Unreleased] section is missing in an existing changelog, the current logic prepends the new version section to the very beginning of the file using join_changelog_head_tail(&section, &without_existing). This is problematic if the file contains a title (e.g., # Changelog) or introductory text, as the new version will be placed before them.

Consider either erroring out (as is done for HEAD updates) or appending to the end of the file as a safer fallback if the anchor section is missing.

Comment thread src/generate.rs
Comment on lines 2312 to 2317
if !dry_run {
let full = join_changelog_head_tail(&updated_head, tail);
let content = format!("{}\n", full.trim_end());
let content =
upsert_version_changelog(&existing, &ctx.tag, &date, &release_url, &parsed.changelog)?;
xx::file::write(&changelog_path, content)?;
info!("wrote {}", changelog_path.display());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The upsert_version_changelog logic is currently skipped entirely during a dry run. It is better to perform the upsert calculation regardless of the dry_run flag to ensure that any potential errors (such as multiple [Unreleased] sections) are caught and reported to the user before an actual run. This also maintains consistency with how the HEAD update is handled earlier in this function.

Suggested change
if !dry_run {
let full = join_changelog_head_tail(&updated_head, tail);
let content = format!("{}\n", full.trim_end());
let content =
upsert_version_changelog(&existing, &ctx.tag, &date, &release_url, &parsed.changelog)?;
xx::file::write(&changelog_path, content)?;
info!("wrote {}", changelog_path.display());
}
let content =
upsert_version_changelog(&existing, &ctx.tag, &date, &release_url, &parsed.changelog)?;
if !dry_run {
xx::file::write(&changelog_path, content)?;
info!("wrote {}", changelog_path.display());
}

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR replaces the LLM-driven CHANGELOG.md rewrite with a fully deterministic upsert_version_changelog helper that removes any existing section for the target version, then inserts the new one immediately after the [Unreleased] block. Style detection (format_version_header), error-on-missing-[Unreleased], and duplicate-[Unreleased] validation are all well-covered by the new tests. The only P2 items are a stale docstring on the now-test-only split_changelog and a redundant double strip_prefix('v') between upsert_version_changelog and format_version_header.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues; only minor P2 style findings that don't affect correctness.

All changed logic is unit-tested, error paths are validated, file writes are guarded behind error propagation, and the [Unreleased] insertion logic is correct. Only two P2 issues (stale doc, redundant strip) which don't affect runtime behaviour.

No files require special attention.

Important Files Changed

Filename Overview
src/generate.rs Replaces the LLM-driven changelog rewrite with deterministic upsert_version_changelog; adds format_version_header, remove_version_section, and three new async tests. split_changelog is correctly gated to #[cfg(test)] but retains a stale LLM-tokens docstring. No P0/P1 issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[update_changelog called] --> B{is_unreleased_head?}
    B -- yes --> C[replace_unreleased_section]
    B -- no --> D[read CHANGELOG.md\nor use default skeleton]
    D --> E[upsert_version_changelog]
    E --> F[repair_changelog_section_boundaries]
    F --> G[format_version_header\ndetect style from existing]
    G --> H[remove_version_section\nstrip old entry if present]
    H --> I[find_unreleased_section]
    I -- None --> J[return Err: missing Unreleased section]
    I -- Some --> K[splice: before + section + after]
    K --> L{dry_run?}
    L -- no --> M[write CHANGELOG.md]
    L -- yes --> N[return Ok]
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(generate): validate release changelo..." | Re-trigger Greptile

Comment thread src/generate.rs Outdated
Comment thread src/generate.rs Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1b8e3d8. Configure here.

Comment thread src/generate.rs
@jdx jdx merged commit e43fdb6 into main Apr 26, 2026
8 of 9 checks passed
@jdx jdx deleted the codex/fix-changelog-release-order branch April 26, 2026 15:08
jdx added a commit that referenced this pull request Apr 26, 2026
A patch release that takes the LLM out of the loop when editing
`CHANGELOG.md` for tagged releases, fixes a section-boundary bug that
could mash release sections together, and adds musl Linux binaries to
the release matrix.

## Fixed

- **Place tagged release entries deterministically** — `communique
generate vX.Y.Z --changelog` no longer asks the model to rewrite
`CHANGELOG.md`. Instead, it parses the existing file, removes any prior
section for the target version, and inserts the new section immediately
after `## [Unreleased]` / `## Unreleased`. The new
`format_version_header` helper infers the existing header style (linked
`## [1.1.0](url) - date`, plain `## 1.1.0`, bracketed `## [1.1.0]`, with
or without dates) and matches it for the new entry, while the generated
changelog body is used verbatim. If the file is missing an Unreleased
anchor, the command now fails with an actionable error rather than
guessing a placement, and duplicate Unreleased sections are caught even
in `--dry-run`. ([#129](#129))
(@jdx)
- **Preserve changelog section boundaries** — Fixed a bug where
`--changelog` updates could concatenate a preserved release section onto
the previous bullet, producing lines like `...))## [1.0.4]`. Only
version-shaped `##` headings are now treated as release boundaries
(category headings like `## Added` / `## Fixed` are no longer mistaken
for them), regenerated heads and preserved tails are joined with an
explicit blank line, and any pre-existing joined boundaries are repaired
before writing. The malformed joins already present in this repo's own
`CHANGELOG.md` were also cleaned up.
([#126](#126)) (@jdx)

## Added

- **musl Linux release assets** — Future GitHub releases now include
`communique-x86_64-unknown-linux-musl.tar.gz` and
`communique-aarch64-unknown-linux-musl.tar.gz` alongside the existing
GNU Linux, macOS, and Windows builds.
([#127](#127)) (@jdx)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this PR only updates release metadata (changelog,
crate/package versions, and generated CLI docs) with no functional code
changes.
> 
> **Overview**
> Adds a new `v1.1.2` entry to `CHANGELOG.md` describing fixes to
deterministic `--changelog` updates and section-boundary preservation,
plus added musl Linux release artifacts.
> 
> Bumps the project version from `1.1.1` → `1.1.2` in
`Cargo.toml`/`Cargo.lock`, the usage spec (`communique.usage.kdl`), and
generated CLI documentation (`docs/cli`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
98c3f94. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant