Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.

Fix automatic release changelog promotion - #23

Merged
jnyross merged 3 commits into
mainfrom
fix/release-consumes-unreleased
Aug 9, 2026
Merged

jnyross merged 3 commits into
mainfrom
fix/release-consumes-unreleased

Conversation

@jnyross

@jnyross jnyross commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Promote only exact ## Unreleased and ## [Unreleased] headings.
  • Preserve the full curated Markdown body byte-for-byte.
  • Normalize generated and curated stems by removing detail text and then an optional PR suffix, so enriched curated bullets suppress represented generated subjects.
  • Ignore level-two headings inside fenced Markdown when locating the next real changelog section.
  • Append every unmatched generated subject after the preserved curated body.

Validation

  • GitHub Windows validation reruns after each correction.
  • Added behavior regressions for all reviewed defects.
  • If no regression case is appropriate, explained why below.

Test notes

  • python3 scripts/test_auto_release.py -v — 11 tests passed: the existing 8 plus 3 exact review regressions.
  • Regressions cover enriched fix: one (#9) — curated details, fenced ## literal heading, and malformed ## [Unreleased.
  • End-to-end temporary Git repository smoke produced 0.6.0, preserved the fenced curated body, suppressed the represented fix, and appended the unrelated feature once.

Note

Low Risk
Changes are confined to the release script and its unit tests; behavior only affects how CHANGELOG.md is written during automated releases, not runtime product code.

Overview
Release changelog handling no longer always prepends a fresh version block from commit subjects. When CHANGELOG.md has a valid ## Unreleased or ## [Unreleased] section, rewrite_release_files promotes that section in place to ## [version] - date, removes the Unreleased heading, and leaves curated Markdown (subsections, em-dash details) unchanged.

Deduping compares normalized “stems” (strip — … detail suffixes and trailing (#PR)), so a generated subject like fix: one (#9) does not duplicate a curated - fix: one — … line. Unmatched generated subjects are appended once; an empty Unreleased body still gets the full generated bullet list. Section boundaries ignore ## lines inside fenced code blocks; malformed ## [Unreleased headings are not promoted and the old prepend path remains.

Tests add a shared fixture helper and regression cases for promotion, append-only extras, PR-suffix matching, fences, bracketed headings, and empty Unreleased.

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

Summary by CodeRabbit

  • New Features
    • Automatically promotes content from an “Unreleased” section into the next release.
    • Supports case-insensitive and bracketed “Unreleased” headings.
    • Preserves curated details and categorized entries while adding missing release items.
    • Generates release entries when the section is empty.
  • Bug Fixes
    • Prevents duplicate entries and preserves literal headings in fenced content.
    • Retains existing behavior when no “Unreleased” section is present.

@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_088218e8-05a6-4652-a53b-6eb9816f5009)

@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The auto-release script detects plain and bracketed Unreleased sections, promotes their content into the release, and generates entries for empty sections. Tests cover subject normalization, duplicate handling, fenced headings, malformed headings, and shared release fixtures.

Changes

Unreleased changelog promotion

Layer / File(s) Summary
Detect and promote Unreleased sections
scripts/auto_release.py
The script normalizes release subjects, ignores headings inside Markdown fences, promotes populated or empty Unreleased sections, and retains the fallback insertion path when no section exists.
Validate promotion behavior
scripts/test_auto_release.py
Tests cover curated details, duplicate suppression, fenced and malformed headings, plain and bracketed headings, empty sections, and reusable release fixtures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant rewrite_release_files
  participant promote_unreleased
  participant Changelog
  rewrite_release_files->>promote_unreleased: pass changelog and release data
  promote_unreleased->>Changelog: replace Unreleased heading or empty section
  promote_unreleased-->>rewrite_release_files: return promoted changelog
  rewrite_release_files->>Changelog: write updated content
Loading

Possibly related PRs

  • jnyross/codex-graph#22: The changes are directly related to consuming and promoting Unreleased changelog entries.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: automatic release changelog promotion.
Description check ✅ Passed The description covers the behavior, validation, regression tests, and test notes, although it does not explain the unchecked no-regression option.
✨ 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-consumes-unreleased

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

coderabbitai[bot]

This comment was marked as resolved.

@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_95db216f-2cd9-48aa-845a-7f0ff9f11144)

coderabbitai[bot]

This comment was marked as resolved.

@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0ea748dd-dcdd-4c9a-9a3b-a0aae427223c)

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/auto_release.py`:
- Around line 135-143: Update the curated_stems comprehension to recognize
Markdown unordered-list prefixes for -, *, and + at any indentation level, strip
the matched prefix and surrounding whitespace before calling
release_subject_stem, and preserve ignoring non-list lines. Add regression
coverage proving * and + curated entries suppress matching generated subjects.
- Around line 125-128: Update the changelog section lookup around
UNRELEASED_HEADING and find_next_changelog_section to scan from the beginning
while tracking fenced-code state, selecting only an Unreleased heading outside
fences. Add a regression case covering a changelog with only a fenced Unreleased
heading before an existing release heading, ensuring the fallback insertion path
is used.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 611b61cb-fe6a-4a0a-bd7a-13ae93a9c0cf

📥 Commits

Reviewing files that changed from the base of the PR and between 0e143f8 and fa1cd3e.

📒 Files selected for processing (2)
  • scripts/auto_release.py
  • scripts/test_auto_release.py

Comment thread scripts/auto_release.py
Comment on lines +125 to +128
heading = UNRELEASED_HEADING.search(changelog)
if heading is None:
return None
section_end = find_next_changelog_section(changelog, heading.end())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ignore fenced Unreleased headings when locating the section.

UNRELEASED_HEADING.search(changelog) can select ## Unreleased inside a fenced code block. find_next_changelog_section starts after that match, so it cannot recover the existing fence state. The release can promote code-example text instead of using the fallback insertion path.

Scan from the start of changelog while tracking fences before selecting an Unreleased heading. Add a regression case with only a fenced ## Unreleased heading before an existing release heading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/auto_release.py` around lines 125 - 128, Update the changelog section
lookup around UNRELEASED_HEADING and find_next_changelog_section to scan from
the beginning while tracking fenced-code state, selecting only an Unreleased
heading outside fences. Add a regression case covering a changelog with only a
fenced Unreleased heading before an existing release heading, ensuring the
fallback insertion path is used.

Comment thread scripts/auto_release.py
Comment on lines +135 to +143
curated_stems = {
release_subject_stem(line[2:].strip())
for line in body.splitlines()
if line.startswith("- ")
}
unmatched = [
subject
for subject in subjects
if release_subject_stem(subject) not in curated_stems

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recognize all supported Markdown bullet markers.

Line 138 only recognizes top-level - entries. A curated * fix: one, + fix: one, or indented list entry does not suppress the matching generated subject. The release then duplicates a curated change.

Match Markdown list prefixes before passing the entry text to release_subject_stem. Add regression coverage for at least * and + entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/auto_release.py` around lines 135 - 143, Update the curated_stems
comprehension to recognize Markdown unordered-list prefixes for -, *, and + at
any indentation level, strip the matched prefix and surrounding whitespace
before calling release_subject_stem, and preserve ignoring non-list lines. Add
regression coverage proving * and + curated entries suppress matching generated
subjects.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread scripts/auto_release.py
Comment on lines +125 to +128
heading = UNRELEASED_HEADING.search(changelog)
if heading is None:
return None
section_end = find_next_changelog_section(changelog, heading.end())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changelog can be rewritten in the wrong place when an example heading appears in a code block

The changelog's "Unreleased" heading is located with a plain text search (UNRELEASED_HEADING.search at scripts/auto_release.py:125) that ignores fenced code blocks and already-published sections, so a sample heading inside an example block gets overwritten with the new release title and the real release notes end up in the wrong place.
Impact: An automated release can silently mangle the published changelog, replacing example text and leaving the new release entries buried at the bottom of the file.

Fence-aware section-end scan is not mirrored by the heading search

find_next_changelog_section (scripts/auto_release.py:89-116) carefully skips ## headings inside Markdown fences, but the search for the Unreleased heading itself (scripts/auto_release.py:125) uses a plain multiline regex over the entire document. Any ## Unreleased line inside a fenced example (e.g. a past release entry that documents the changelog format), or any stray ## Unreleased located below existing release sections, is matched.

Reproduced with the current code:

## [0.3.0] - 2026-01-02

- feat: document the changelog format, e.g.

```markdown
## Unreleased

- note

becomes

## [1.0.0] - d

- note

...

  • Old

  • feat: a


so the example is destroyed, the new heading is inside a code fence, and the generated subject is appended after the oldest release.

A fix would be to reuse the fence-tracking scan when locating the heading (e.g. walk lines from the start of the document tracking fences and stop at the first non-fenced `## [Unreleased]`/`## Unreleased`), and/or to only accept a heading that occurs before the first `## [` release section.

</details>

<details>
<summary>Prompt for agents</summary>

In scripts/auto_release.py, promote_unreleased locates the Unreleased heading with a plain regex search over the whole changelog, while find_next_changelog_section deliberately tracks Markdown code fences when locating the end of the section. This asymmetry means an ## Unreleased line that appears inside a fenced code example (for instance a past release entry documenting the changelog format) is treated as the real Unreleased heading: it gets replaced by the new release heading inside the fence and the generated subjects are appended far below, corrupting the file. The same issue applies to an ## Unreleased heading that appears after existing ## [x.y.z] release sections, which would place the new release out of order. Consider reusing the fence-aware line walk to find the heading (only accepting a heading outside any fence), and optionally requiring that it appear before the first published release section.


</details>

<!-- devin-review-badge-begin -->
<a href="https://app.devin.ai/review/jnyross/codex-graph/pull/23" target="_blank">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
    <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review">
  </picture>
</a>
<!-- devin-review-badge-end -->

---
*Was this helpful? React with 👍 or 👎 to provide feedback.*

@jnyross
jnyross merged commit cda2fb1 into main Aug 9, 2026
4 checks passed
@jnyross
jnyross deleted the fix/release-consumes-unreleased branch August 9, 2026 23:02
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant