Skip to content

(PE-47992) Preserve released changelog sections in module release prep - #194

Open
Jade2153 wants to merge 1 commit into
mainfrom
PE-47992/main/changelog-history-guard
Open

Jade2153 wants to merge 1 commit into
mainfrom
PE-47992/main/changelog-history-guard

Conversation

@Jade2153

@Jade2153 Jade2153 commented Nov 11, 2026 •

Copy link
Copy Markdown

Summary

Jira: PE-47992

gh changelog new regenerates the whole CHANGELOG.md from every closed PR and tag, so each module_release_prep run rewrites and backfills sections of versions that were already released. On puppetlabs-peadm PR #749 that removed an existing entry, inserted Revert lines for PRs merged after those releases, and reclassified headers in [v3.38.2]/[v3.38.3]. Each release needed a manual diff-and-revert.

gh-changelog has no flag that fixes this. --latest builds a one-entry changelog and --from-version still regenerates history, so this takes the ticket's second suggestion: a guard after the generator.

Changes

  • New step "Preserve changelog history" in module_release_prep.yml, right after "Generate changelog". It keeps the generated preamble and newest section and restores every older section from the committed CHANGELOG.md (HEAD). A re-run for a version already on top replaces that section instead of duplicating it.
  • Fails instead of losing history when:
    • the version is not a plain version (1.2.3, optional v, one -pre or +build suffix);
    • the generated changelog is missing or has no ## [version] sections;
    • the newest generated section is not the requested version;
    • the committed changelog has ATX headings but no ## sections, unless it is only a bare title such as # Changelog.
  • A committed Unreleased section is dropped with a ::warning::.
  • "Generate changelog" and "Update metadata.json" now read the version from inputs.version through env:. They previously interpolated ${{ github.event.inputs.version }} into the script. In a workflow_call workflow that is the caller's event payload, and it is only set when the caller is a workflow_dispatch with an input named version. This also removes the script injection. A leading v is stripped for the generator and metadata.json, which previously received vv1.2.3 and v1.2.3.
  • tests/module_release_prep/test_changelog_guard.sh extracts the step from the workflow and runs it in throwaway git repos under bash -e with and without pipefail: 127 checks covering the ticket's three symptoms, re-runs, CRLF, tab and other header styles, and every failure path. It also asserts the version wiring and step order. lint.yml now runs it, with permissions: contents: read.
  • README.md and CLAUDE.md updated to match.

Behaviour changes to be aware of

  • A run now fails where it used to succeed if the generator emits no section for the requested version, for example when nothing has merged since the last tag. Before, that produced a rewritten changelog and a PR. I have not confirmed whether gh-changelog ever omits the section.
  • Callers whose version input was not a workflow_dispatch input called version now get the value they actually pass.

Known limitations

Section splitting is line based on ## : lines inside code fences count as headers, and text before the first section or under a committed Unreleased heading is dropped. Changelogs in other styles (setext, plain text, indented headings) keep the generated file with a warning. Legacy headers such as ## Version 1.0.0 are not recognised as the prepared version on a re-run, which leaves a stale duplicate and loses nothing.

Not in this PR

gem_release_prep.yml has the same gh changelog new step and the same history rewrite, and still interpolates github.event.inputs.version. It is left for a follow-up.

Testing

  • tests/module_release_prep/test_changelog_guard.sh: 127 checks pass on macOS (BSD awk) and ubuntu:24.04 (mawk 1.3.4, GNU grep).
  • yamllint -c .yamllint .github/ is clean on ubuntu:24.04.
  • Not run end to end through GitHub Actions: the workflow only runs when a module calls it, so the first real run will be the next module release prep.

Related Issues

  • PE-47992. The symptom was first seen on puppetlabs-peadm PR #749.

Checklist

  • 🟢 Spec tests: tests/module_release_prep/test_changelog_guard.sh, run by lint.yml.
  • 🟢 Acceptance tests: none exist for these reusable workflows, and it has not been run end to end through a module's release prep.
  • Manually verified: not run against a real module release prep yet.

🤖 Generated with Claude Code

gh-changelog regenerates the whole CHANGELOG.md on every run, which rewrites
and backfills sections of versions that were already released. Add a
"Preserve changelog history" step after "Generate changelog" that keeps the
generated preamble and newest section and restores the previously released
sections from the committed CHANGELOG.md.

The step fails instead of losing history when the version is invalid, the
generated changelog has no version sections or its newest section is not the
requested version, or the committed changelog has headings it cannot restore.
A committed Unreleased section is dropped with a warning.

"Generate changelog" and "Update metadata.json" now read the version from
inputs.version through the environment instead of interpolating
github.event.inputs.version into the script.

tests/module_release_prep/test_changelog_guard.sh runs the step against
fixtures, under bash -e with and without pipefail, and lint.yml runs it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI 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.

🟡 Changes recommended

The workflow can advance metadata without a matching changelog section when the committed changelog is missing or title-only, and its version validation rejects valid SemVer inputs containing both prerelease and build metadata.

2 open issues
What changed in this PR

This PR adds changelog-history preservation to the reusable module release-prep workflow, aiming to prevent regeneration from rewriting previously released sections.

Changes:

  • Restores committed release history while keeping the newly generated release section, with validation and re-run handling.
  • Reads the requested version through environment variables and normalizes a leading v.
  • Adds regression checks to CI and documents the new behavior.
File Description
tests/​module_release_prep/​test_changelog_guard.sh Adds preservation, validation, and version-wiring regression checks.
README.md Documents changelog preservation and failure behavior.
CLAUDE.md Documents CI test coverage and the local test command.
.github/​workflows/​module_release_prep.yml Adds history restoration and updates version handling.
.github/​workflows/​lint.yml Runs the regression checks with read-only repository permissions.

Review effort: Balanced

Comment on lines +86 to +89
if [ ! -f CHANGELOG.md ] || ! grep -q '^## \[' CHANGELOG.md; then
echo "::error::Generated CHANGELOG.md is missing or has no version sections, refusing to drop the committed history"
exit 1
fi
run: |
# Reject anything but a plain version, which also keeps it safe to use in
# the sed expression of the "Update metadata.json" step
if [[ "$VERSION" == *$'\n'* ]] || ! [[ "$VERSION" =~ ^v?[0-9]+(\.[0-9]+)*([-+][0-9A-Za-z.-]+)?$ ]]; then
file_or_dir: ".github/"
config_file: ".yamllint"

- name: "Test module_release_prep changelog preservation"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why does lint run this?

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.

3 participants