Skip to content

fix(scripts): rotate missed boundary footers in changelog housekeeping (#717)#718

Merged
doobidoo merged 3 commits intomainfrom
fix/717-housekeeping-footer-boundaries
Apr 15, 2026
Merged

fix(scripts): rotate missed boundary footers in changelog housekeeping (#717)#718
doobidoo merged 3 commits intomainfrom
fix/717-housekeeping-footer-boundaries

Conversation

@doobidoo
Copy link
Copy Markdown
Owner

Summary

Closes #717. PR #716 had to patch two version-boundary strings by hand because `scripts/maintenance/changelog_housekeeping.py` only matched bolded-header patterns. This PR teaches the script to update both remaining formats automatically.

Fixes

Two new helpers alongside the existing `update_header_range()`:

  • `update_archive_header_boundary()` — matches the plain-prose line in `docs/archive/CHANGELOG-HISTORIC.md`:
    ```
    Older changelog entries for MCP Memory Service (vX.Y.Z and earlier).
    ```
  • `update_readme_footer_boundary()` — matches the markdown-link text in the README "Full version history" footer:
    ```markdown
    ... | Older versions (vX.Y.Z and earlier) | ...
    ```

Both wired into `run()` at the points where the archive header and README are already being rewritten. Both use `newest_archived` (the oldest version not kept in CHANGELOG.md) — that's the correct boundary for someone following the "Older versions" link.

Tests

`tests/maintenance/test_changelog_housekeeping.py` — 7/7 passing:

  • Each helper rewrites its target format correctly
  • Idempotent (re-running with the current boundary is a no-op)
  • No-op on unrelated prose that happens to contain "and earlier"
  • Regression: existing `update_header_range()` still matches bolded headers after the refactor

Verification

`python scripts/maintenance/changelog_housekeeping.py --dry-run` against current main: correctly reports "No housekeeping needed" (90 lines / 7 entries, below thresholds). Next rotation won't need manual Gemini follow-up for these footers.

Changes

  • `scripts/maintenance/changelog_housekeeping.py`: +43 lines (two helpers + two call sites)
  • `tests/maintenance/test_changelog_housekeeping.py`: +76 lines (new file)

🤖 Generated with Claude Code

#717)

PR #716 had to patch two version boundaries by hand because the
housekeeping script's regex only matched the bolded-header patterns:

- docs/archive/CHANGELOG-HISTORIC.md prose header
  "Older changelog entries for MCP Memory Service (vX.Y.Z and earlier)."
- README.md "Full version history" footer's markdown-link text
  "[Older versions (vX.Y.Z and earlier)](docs/archive/CHANGELOG-HISTORIC.md)"

Added two sibling helpers — update_archive_header_boundary() and
update_readme_footer_boundary() — that target each format specifically,
and wired them into run() at the points where the archive header and
README are already being rewritten. Both target the newest-archived
version, which is the correct boundary for a reader following the link.

Tests: 7/7 in tests/maintenance/test_changelog_housekeeping.py cover:
boundary rewriting for each format, idempotence, no-op on unrelated
prose, regression check that the existing update_header_range()
continues to match bolded headers.

Closes #717

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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 introduces two new helper functions, update_archive_header_boundary and update_readme_footer_boundary, to the changelog housekeeping script to automate the updating of version boundaries in the archive documentation and README footer. It also includes a comprehensive suite of unit tests to verify these changes. The review feedback suggests improving the robustness of the regex patterns in both new functions by making the version boundary optional, which allows the script to correctly insert the version information even if it was previously missing or formatted differently.

Comment on lines +137 to +143
pattern = re.compile(
r"(Older changelog entries for MCP Memory Service )\(v[\d.]+ and earlier\)"
)
return pattern.sub(
rf"\1(v{newest_archived} and earlier)",
archive_header,
)
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.

medium

The regex currently requires the version boundary (vX.Y.Z and earlier) to be present for a match to occur. Making this part optional and using a more flexible whitespace match ensures the helper can correctly insert the boundary even if it was previously missing or formatted differently in the archive header.

Suggested change
pattern = re.compile(
r"(Older changelog entries for MCP Memory Service )\(v[\d.]+ and earlier\)"
)
return pattern.sub(
rf"\1(v{newest_archived} and earlier)",
archive_header,
)
pattern = re.compile(
r"(Older changelog entries for MCP Memory Service)\s*(?:\(v[\d.]+ and earlier\))?"
)
return pattern.sub(
rf"\1 (v{newest_archived} and earlier)",
archive_header,
)

Comment on lines +156 to +162
pattern = re.compile(
r"(\[Older versions )\(v[\d.]+ and earlier\)(\]\([^)]+\))"
)
return pattern.sub(
rf"\1(v{newest_archived} and earlier)\2",
text,
)
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.

medium

The current regex requires the version boundary to already exist in the link text. However, the trim_readme_previous_releases function (at line 244) uses a fallback footer template that lacks this boundary. If the script ever has to recreate a missing footer, this helper will fail to update it with the correct version. Making the version part of the regex optional ensures the boundary is added correctly in all cases.

Suggested change
pattern = re.compile(
r"(\[Older versions )\(v[\d.]+ and earlier\)(\]\([^)]+\))"
)
return pattern.sub(
rf"\1(v{newest_archived} and earlier)\2",
text,
)
pattern = re.compile(
r"(\[Older versions)\s*(?:\(v[\d.]+ and earlier\))?(\]\([^)]+\))"
)
return pattern.sub(
rf"\1 (v{newest_archived} and earlier)\2",
text,
)

Copy link
Copy Markdown

@xkonjin xkonjin 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

General

Tidy maintenance fix. The regex helpers are well-scoped, the tests are thorough, and the idempotency coverage is appreciated.

Bugs

  • Regex precision on update_archive_header_boundary: The pattern hardcodes MCP Memory Service. If the project name is ever rebranded or used as a template, this will silently no-op. Given it's a repo-specific maintenance script, that's acceptable, but consider adding a quick assertion in the test suite that at least one substitution occurs when running against the real file.
  • Regex greediness on update_readme_footer_boundary: The pattern [Older versions )(v[\d.]+ and earlier)(\]([^)]+)) is safe here because the link text is tightly bounded, but be aware that [^)]+ in the URL group will break if the URL ever contains a closing paren (e.g., query strings with )). Unlikely in this repo, so not a blocker.

Testing

  • Real-file validation: The dry-run verification is good, but consider a CI step that runs python scripts/maintenance/changelog_housekeeping.py --dry-run on PRs touching the script so regressions are caught before merge.
  • 7/7 tests passing: Coverage looks solid. The no-op cases (unrelated prose, idempotency) are the most important guardrails for a script that mutates documentation.

Minor

  • The docstrings and comments clearly explain why each helper exists. Keep that standard.

Overall: LGTM. Low risk, high maintenance-value fix.

Gemini review on PR #718 pointed out that the boundary regexes would
fail if the existing text lacked a "(vX.Y.Z and earlier)" suffix —
relevant because trim_readme_previous_releases() recreates a fallback
footer without that suffix when the section would otherwise vanish.

Made the version-range group optional in both regexes and moved the
leading space into the replacement so the helpers now *insert* the
boundary when it's missing instead of silently no-op'ing.

Added two regression tests for the insert-when-missing path. 9/9
passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@doobidoo
Copy link
Copy Markdown
Owner Author

Iter 2 (ea16745): Gemini medium findings fixed.

Both boundary regexes now make the (vX.Y.Z and earlier) suffix optional, so the helpers can also insert the boundary when it's missing — relevant because trim_readme_previous_releases recreates a fallback footer without the suffix when the section would otherwise vanish.

Two regression tests added for the insert-when-missing path. 9/9 tests passing.

/gemini review

Copy link
Copy Markdown
Contributor

@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 introduces two helper functions, update_archive_header_boundary and update_readme_footer_boundary, to automate version boundary updates in the historic changelog and README footer, supported by a new unit test suite. The review feedback highlights that the regex patterns used for version matching are too restrictive, failing on pre-release versions and potentially causing string duplication; suggestions were provided to use more inclusive patterns.

# Boundary suffix is optional so the helper can also insert it when missing
# (e.g. a recreated fallback header without the version range).
pattern = re.compile(
r"(Older changelog entries for MCP Memory Service)(?:\s*\(v[\d.]+ and earlier\))?"
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.

medium

The regex pattern for matching the version boundary is somewhat restrictive and potentially buggy. Using [\d.]+ will fail to match versions with pre-release identifiers (e.g., v1.0.0-beta). More importantly, because the version group is optional and lacks a trailing anchor, if the existing version string doesn't match the pattern exactly, the regex will match only the prefix and append the new version, resulting in a duplicated version string (e.g., ... (v10.36.3 and earlier) (v1.0.0-beta and earlier)).

Suggested change
r"(Older changelog entries for MCP Memory Service)(?:\s*\(v[\d.]+ and earlier\))?"
r"(Older changelog entries for MCP Memory Service)(?:\s*\(v[^)]+ and earlier\))?"

# fallback footer that trim_readme_previous_releases recreates when the
# "Previous Releases" section ends up bare.
pattern = re.compile(
r"(\[Older versions)(?:\s*\(v[\d.]+ and earlier\))?(\]\([^)]+\))"
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.

medium

Similar to the archive header helper, using [\d.]+ to match the version string is restrictive and will fail for versions containing non-numeric characters (like -rc1 or -beta). While the trailing anchor ()( [^)]+)) prevents the "double version" bug here, it would still cause the update to fail (no-op) if the version format deviates from simple digits and dots.

Suggested change
r"(\[Older versions)(?:\s*\(v[\d.]+ and earlier\))?(\]\([^)]+\))"
r"(\[Older versions)(?:\s*\(v[^)]+ and earlier\))?(\]\([^)]+\))"

…on bug

Gemini iter 2 on PR #718 caught two defensive-coding gaps:

1. Version group used `[\d.]+`, rejecting pre-release / build-metadata
   identifiers like 1.0.0-beta, 1.0.0-rc.1, 1.0.0+build.5. Broadened to
   `[\w.+-]+` so the helpers don't no-op when users follow the full
   SemVer spec.

2. The archive-header helper had no trailing anchor for the optional
   version group. If a pre-release version slipped past the narrow
   `[\d.]+` class, the prefix would still match while the boundary
   group didn't — producing a duplicate "(vX and earlier) (vY and
   earlier)." chain. Added the required trailing period as the anchor:
   the boundary is always followed by `.` in the archive header, so
   this pins the match to a complete boundary (or forces no-op).

   The README helper already has a strong anchor (the markdown-link
   URL part), so only the version class needed broadening there.

Tests (12/12): added three regression cases — pre-release rewrite for
each helper, and a "no double-version on unparseable boundary" check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@doobidoo
Copy link
Copy Markdown
Owner Author

Iter 3 (2fbe946) — final iteration per plan.

Both Gemini medium findings addressed:

  1. Pre-release versions: [\d.]+[\w.+-]+ so SemVer pre-release/build-metadata (1.0.0-beta, 1.0.0-rc.1, 1.0.0+build.5) rewrites correctly instead of silently no-op'ing.
  2. Double-version bug in archive helper: added trailing . as required anchor on the archive-header regex — the boundary is always period-terminated, so this pins the match to a complete boundary or forces a full no-op. README helper already had the markdown URL as strong anchor.

3 new regression tests: pre-release rewrite for each helper + "no double-version on unparseable boundary" guard. 12/12 passing.

@doobidoo doobidoo merged commit db129d3 into main Apr 15, 2026
7 checks passed
@doobidoo doobidoo deleted the fix/717-housekeeping-footer-boundaries branch April 15, 2026 20:24
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.

fix(scripts): changelog_housekeeping misses two boundary footers

2 participants