-
-
Notifications
You must be signed in to change notification settings - Fork 40
collate_changelog: rerun after partial failure duplicates the folded section (bot finding #2320/F4) #2474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
collate_changelog: rerun after partial failure duplicates the folded section (bot finding #2320/F4) #2474
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ### Fixed | ||
| - `scripts/collate_changelog.py` is now idempotent across partial failures: if a run dies between writing the new version section and unlinking consumed fragments, a rerun detects the existing `## [<version>]` header and skips the duplicate insert. Only leftover fragments whose content already reached `CHANGELOG.md` are consumed; a fragment that landed after the failed run is kept and the rerun exits non-zero naming it, instead of silently deleting a release note that was never folded. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,3 +114,119 @@ def test_missing_unreleased_anchor_fails_loudly_and_keeps_fragments(repo: Path): | |
| assert mod.main(["1.0.0-beta.47"]) == 1 | ||
| # The fragment survives a failed run so nothing is lost. | ||
| assert (repo / "changelog.d" / "2295-z.md").exists() | ||
|
|
||
|
|
||
| def test_rerun_after_partial_unlink_failure_is_idempotent(repo: Path, monkeypatch): | ||
| mod = _load(repo) | ||
| (repo / "changelog.d" / "2291-notes.md").write_text("- Notes area (#2291).\n", encoding="utf-8") | ||
|
|
||
| fail_once = True | ||
| import pathlib | ||
|
|
||
| _real_unlink = pathlib.Path.unlink | ||
|
|
||
| def fake_unlink(self): | ||
| nonlocal fail_once | ||
| if fail_once: | ||
| fail_once = False | ||
| raise OSError("simulated unlink failure") | ||
| _real_unlink(self) | ||
|
|
||
| monkeypatch.setattr(pathlib.Path, "unlink", fake_unlink) | ||
|
|
||
| # First run: writes the section, then fails on unlink (exception propagates). | ||
| with pytest.raises(OSError, match="simulated unlink failure"): | ||
| mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) | ||
|
|
||
| # Second run: fragment is still present, so without idempotency the section | ||
| # would be inserted a second time. | ||
| assert mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) == 0 | ||
|
|
||
| text = (repo / "CHANGELOG.md").read_text(encoding="utf-8") | ||
| assert text.count("## [1.0.0-beta.47]") == 1 | ||
| assert "- Notes area (#2291)." in text | ||
| assert not (repo / "changelog.d" / "2291-notes.md").exists() | ||
|
|
||
|
|
||
| def test_rerun_keeps_fragment_that_landed_after_the_partial_failure(repo: Path, monkeypatch): | ||
| """A fragment merged between the failed run and the rerun was never folded. | ||
|
|
||
| The rerun must not silently unlink it: its content exists nowhere in | ||
| CHANGELOG.md, so deleting it loses the release note. Stale (already | ||
| folded) leftovers are still consumed; the unfolded one is kept and the | ||
| rerun refuses loudly so the operator folds it under the right version. | ||
| """ | ||
| mod = _load(repo) | ||
| (repo / "changelog.d" / "2291-notes.md").write_text("- Notes area (#2291).\n", encoding="utf-8") | ||
|
|
||
| fail_once = True | ||
| import pathlib | ||
|
|
||
| _real_unlink = pathlib.Path.unlink | ||
|
|
||
| def fake_unlink(self): | ||
| nonlocal fail_once | ||
| if fail_once: | ||
| fail_once = False | ||
| raise OSError("simulated unlink failure") | ||
| _real_unlink(self) | ||
|
|
||
| monkeypatch.setattr(pathlib.Path, "unlink", fake_unlink) | ||
|
|
||
| with pytest.raises(OSError, match="simulated unlink failure"): | ||
| mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) | ||
|
|
||
| # A new PR merges its fragment between the failed run and the rerun. | ||
| (repo / "changelog.d" / "2299-new.md").write_text("- Brand new feature (#2299).\n", encoding="utf-8") | ||
|
|
||
| rc = mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) | ||
| text = (repo / "CHANGELOG.md").read_text(encoding="utf-8") | ||
|
|
||
| # The stale leftover was already folded by the first run: consumed. | ||
| assert not (repo / "changelog.d" / "2291-notes.md").exists() | ||
| # The unfolded fragment survives, its content is not lost and not | ||
| # half-inserted anywhere. | ||
| assert (repo / "changelog.d" / "2299-new.md").exists() | ||
| assert "- Brand new feature (#2299)." not in text | ||
| assert text.count("## [1.0.0-beta.47]") == 1 | ||
| # And the rerun says NO loudly instead of pretending it consumed cleanly. | ||
| assert rc == 1 | ||
|
|
||
|
|
||
| def test_rerun_keeps_unfolded_fragment_whose_text_matches_an_older_release(repo: Path, monkeypatch): | ||
| """A late-landing fragment whose bullet also exists under an OLDER release. | ||
|
|
||
| The rerun's folded-check must scope its match to the target version's | ||
| section: a whole-file match sees the older release's identical bullet, | ||
| counts the fragment as folded, and unlinks it -- silently losing the new | ||
| release note. | ||
| """ | ||
| mod = _load(repo) | ||
| (repo / "changelog.d" / "2291-notes.md").write_text("- Notes area (#2291).\n", encoding="utf-8") | ||
|
|
||
| fail_once = True | ||
| import pathlib | ||
|
|
||
| _real_unlink = pathlib.Path.unlink | ||
|
|
||
| def fake_unlink(self): | ||
| nonlocal fail_once | ||
| if fail_once: | ||
| fail_once = False | ||
| raise OSError("simulated unlink failure") | ||
| _real_unlink(self) | ||
|
|
||
| monkeypatch.setattr(pathlib.Path, "unlink", fake_unlink) | ||
|
|
||
| with pytest.raises(OSError, match="simulated unlink failure"): | ||
| mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) | ||
|
|
||
| # A new PR merges a fragment whose text duplicates a bullet ALREADY present | ||
| # in the older 1.0.0-beta.46 section of the fixture changelog. | ||
| (repo / "changelog.d" / "2299-new.md").write_text("- older thing (#1).\n", encoding="utf-8") | ||
|
|
||
| rc = mod.main(["1.0.0-beta.47", "--date", "2026-08-05"]) | ||
|
|
||
| # The unfolded fragment survives and the rerun refuses loudly. | ||
| assert (repo / "changelog.d" / "2299-new.md").exists() | ||
| assert rc == 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: Test should also verify already-folded fragments are consumed on rerun The new test validates that the unfolded fragment ( Reply with |
||
Uh oh!
There was an error while loading. Please reload this page.