Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **A rebase across a release can no longer move changelog entries into a
published version.** When a release is cut while a branch is open, rebasing
that branch drops its entries into the version section that was just
materialized — and because the `### Fixed` / `### Added` headings match on
both sides, git does it without a conflict, so nothing warns and the entries
are silently attributed to a release that never contained them. This happened
on four branches in a row. The changelog gate now compares everything from
the first released version heading down against the base branch and fails if
it differs, naming the fix; a deliberate edit of released history uses a
`docs(changelog)` commit.
- **The e2e VictoriaLogs restarts were OOM kills, and it now has the memory it
needs.** The earlier diagnosis was wrong: `docker inspect` reports the state
of the container that is already running again, which showed `exit 0` and
Expand Down
38 changes: 38 additions & 0 deletions scripts/ci/check_changelog_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,29 @@ def is_dependency_only_pr(commits: Iterable[str], files: Iterable[str]) -> bool:
)


def extract_released_sections(text: str) -> str:
"""Everything from the first released version heading down.

A rebase across a release routinely drops a feature branch's entries into
the version section that was cut while the branch was open, and git does it
without a conflict when the section headings match, so nothing warns. The
released history is immutable once published: comparing this slice against
the base catches that silently.
"""
match = re.search(r"^## \[\d[^\]]*\]", text, re.MULTILINE)
if not match:
return ""
return text[match.start():]


def is_changelog_history_fix(commits: Iterable[str]) -> bool:
"""True when the pull request deliberately edits released history."""
subjects = [c for c in commits if c.strip() and not is_merge_commit(c)]
if not subjects:
return False
return all(c.strip().lower().startswith("docs(changelog)") for c in subjects)


def is_release_metadata_sync(files: Iterable[str]) -> bool:
file_list = [f for f in files if f.strip()]
if not file_list or "CHANGELOG.md" not in file_list:
Expand Down Expand Up @@ -264,6 +287,21 @@ def main() -> int:
print("changelog gate: ok (release metadata sync)")
return 0

if (
"CHANGELOG.md" in files
and not is_changelog_history_fix(commits)
and extract_released_sections(head_text) != extract_released_sections(base_text)
):
print(
"changelog gate: released version sections must match the base branch — "
"entries belong under [Unreleased]. A rebase across a release moves them "
"into the version that was cut while the branch was open, without a "
"conflict. Rebuild CHANGELOG.md from the base and re-insert your entries "
"under [Unreleased] (a deliberate history fix uses a docs(changelog) commit).",
file=sys.stderr,
)
return 1

if not should_require_changelog(commits, files):
print("changelog gate: skipped (no releasable changes detected)")
return 0
Expand Down
27 changes: 27 additions & 0 deletions scripts/ci/tests/test_check_changelog_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
has_genuinely_new_unreleased_entries,
has_meaningful_changelog_content,
has_new_version_sections,
extract_released_sections,
is_changelog_history_fix,
is_dependency_only_pr,
is_release_metadata_sync,
should_require_changelog,
Expand Down Expand Up @@ -108,6 +110,31 @@ def test_dependency_only_pr_go_modules(self):
)
)

def test_extract_released_sections_starts_at_first_version(self):
text = """# Changelog

## [Unreleased]

### Fixed

- something new

## [1.2.0] - 2026-01-01

- released entry
"""
released = extract_released_sections(text)
self.assertTrue(released.startswith("## [1.2.0]"))
self.assertNotIn("something new", released)

def test_extract_released_sections_empty_when_nothing_released(self):
self.assertEqual(extract_released_sections("# Changelog\n\n## [Unreleased]\n"), "")

def test_changelog_history_fix_detects_docs_changelog_commits(self):
self.assertTrue(is_changelog_history_fix(["docs(changelog): move entry back to Unreleased"]))
self.assertFalse(is_changelog_history_fix(["fix: something"]))
self.assertFalse(is_changelog_history_fix([]))

def test_dependency_only_pr_survives_a_branch_update(self):
"""Bringing a dependency branch up to date must not make it releasable."""
self.assertTrue(
Expand Down
Loading