Skip to content

Commit a736361

Browse files
committed
chore(release): sync changelog and version markers to main during backports
Collects news files and version marker diffs during backport cherry-picks, and proposes a PR to main to apply them (with auto-merge). - Add git diff/apply helpers. - Update GH helper for PR creation and auto-merge. - Update changelog utility for selective news and semver sorting. - Update process_backports to orchestrate the sync PR.
1 parent dcbdf1b commit a736361

9 files changed

Lines changed: 766 additions & 57 deletions

File tree

tests/tools/private/release/changelog_news_test.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,95 @@ def test_update_changelog_empty_news(self):
408408
self.assertNotIn("{#v3-0-0-fixed}", new_content)
409409
self.assertNotIn("{#v3-0-0-added}", new_content)
410410

411+
def test_update_changelog_selective_news_files(self):
412+
# Arrange
413+
changelog = """# Changelog
414+
415+
{#unreleased}
416+
## Unreleased
417+
418+
[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased
419+
420+
{#v2-0-2}
421+
## [2.0.2] - 2026-05-14
422+
423+
[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2
424+
"""
425+
changelog_path = self.tmpdir / "CHANGELOG.md"
426+
changelog_path.write_text(changelog)
427+
428+
news_dir = self.tmpdir / "news"
429+
news_dir.mkdir()
430+
431+
# Create news files
432+
(news_dir / "123.fixed.md").write_text("Fix A")
433+
(news_dir / "456.fixed.md").write_text("Fix B")
434+
435+
# Act: Only process 123.fixed.md
436+
changelog_news.update_changelog(
437+
"2.0.3",
438+
"2026-06-16",
439+
changelog_path=changelog_path,
440+
news_dir=news_dir,
441+
news_files=[news_dir / "123.fixed.md"],
442+
)
443+
444+
# Assert
445+
# 1. Only 123.fixed.md should be deleted
446+
self.assertFalse((news_dir / "123.fixed.md").exists())
447+
self.assertTrue((news_dir / "456.fixed.md").exists())
448+
449+
new_content = changelog_path.read_text()
450+
451+
# 2. Only Fix A should be in the changelog
452+
self.assertIn("Fix A", new_content)
453+
self.assertNotIn("Fix B", new_content)
454+
455+
def test_update_changelog_insertion_point(self):
456+
# Arrange
457+
changelog = """# Changelog
458+
459+
{#unreleased}
460+
## Unreleased
461+
462+
[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased
463+
464+
{#v2-2-0}
465+
## [2.2.0] - 2026-06-30
466+
467+
[2.2.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.2.0
468+
469+
{#v2-0-0}
470+
## [2.0.0] - 2026-04-09
471+
472+
[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0
473+
"""
474+
changelog_path = self.tmpdir / "CHANGELOG.md"
475+
changelog_path.write_text(changelog)
476+
477+
news_dir = self.tmpdir / "news"
478+
news_dir.mkdir()
479+
(news_dir / "123.fixed.md").write_text("Fix in 2.1.0")
480+
481+
# Act: Insert 2.1.0
482+
changelog_news.update_changelog(
483+
"2.1.0",
484+
"2026-06-17",
485+
changelog_path=changelog_path,
486+
news_dir=news_dir,
487+
)
488+
489+
# Assert
490+
new_content = changelog_path.read_text()
491+
492+
# Verify 2.1.0 is inserted BEFORE 2.0.0 but AFTER 2.2.0
493+
idx_2_2_0 = new_content.index("{#v2-2-0}")
494+
idx_2_1_0 = new_content.index("{#v2-1-0}")
495+
idx_2_0_0 = new_content.index("{#v2-0-0}")
496+
497+
self.assertTrue(idx_2_2_0 < idx_2_1_0 < idx_2_0_0)
498+
self.assertIn("Fix in 2.1.0", new_content)
499+
411500

412501
if __name__ == "__main__":
413502
unittest.main()

tests/tools/private/release/git_test.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,88 @@ def test_fetch_all_options(self):
8282
)
8383

8484

85+
class GitGetModifiedFilesTest(unittest.TestCase):
86+
def setUp(self):
87+
self.git = Git(".")
88+
self.patcher = patch.object(self.git, "_run_git")
89+
self.mock_run_git = self.patcher.start()
90+
self.addCleanup(self.patcher.stop)
91+
92+
def test_get_modified_files(self):
93+
self.mock_run_git.return_value = "file1.txt\nfile2.py\n\n"
94+
files = self.git.get_modified_files("HEAD")
95+
self.mock_run_git.assert_called_once_with(
96+
"show", "--name-only", "--format=", "HEAD"
97+
)
98+
self.assertEqual(files, ["file1.txt", "file2.py"])
99+
100+
def test_get_modified_files_empty(self):
101+
self.mock_run_git.return_value = ""
102+
files = self.git.get_modified_files("HEAD")
103+
self.assertEqual(files, [])
104+
105+
106+
class GitDiffTest(unittest.TestCase):
107+
def setUp(self):
108+
self.git = Git(".")
109+
self.patcher = patch.object(self.git, "_run_git")
110+
self.mock_run_git = self.patcher.start()
111+
self.addCleanup(self.patcher.stop)
112+
113+
def test_diff_has_changes(self):
114+
self.mock_run_git.return_value = "some diff output"
115+
output = self.git.diff()
116+
self.mock_run_git.assert_called_once_with("diff")
117+
self.assertEqual(output, "some diff output")
118+
119+
def test_diff_empty(self):
120+
self.mock_run_git.return_value = ""
121+
output = self.git.diff()
122+
self.mock_run_git.assert_called_once_with("diff")
123+
self.assertEqual(output, "")
124+
125+
126+
class GitApplyTest(unittest.TestCase):
127+
def setUp(self):
128+
self.git = Git(".")
129+
self.patcher = patch.object(self.git, "_run_git")
130+
self.mock_run_git = self.patcher.start()
131+
self.addCleanup(self.patcher.stop)
132+
133+
def test_apply(self):
134+
self.git.apply("patch.patch")
135+
self.mock_run_git.assert_called_once_with(
136+
"apply", "patch.patch", capture_output=False
137+
)
138+
139+
140+
class GitApplyCheckTest(unittest.TestCase):
141+
def setUp(self):
142+
self.git = Git(".")
143+
self.patcher = patch.object(self.git, "_run_git")
144+
self.mock_run_git = self.patcher.start()
145+
self.addCleanup(self.patcher.stop)
146+
147+
def test_apply_check_clean(self):
148+
self.mock_run_git.return_value = ""
149+
result = self.git.apply_check("patch.patch")
150+
self.mock_run_git.assert_called_once_with(
151+
"apply", "--check", "patch.patch", capture_output=False
152+
)
153+
self.assertTrue(result)
154+
155+
def test_apply_check_conflict(self):
156+
import subprocess
157+
158+
self.mock_run_git.side_effect = subprocess.CalledProcessError(
159+
1, ["git", "apply", "--check", "patch.patch"]
160+
)
161+
result = self.git.apply_check("patch.patch")
162+
self.mock_run_git.assert_called_once_with(
163+
"apply", "--check", "patch.patch", capture_output=False
164+
)
165+
self.assertFalse(result)
166+
167+
85168
if __name__ == "__main__":
86169
unittest.main()

tests/tools/private/release/prepare_test.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ def test_prepare_success_existing_issue(self, mock_replace, mock_changelog):
3636
self.assertEqual(result, 0)
3737
self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0")
3838
self.mock_gh.create_tracking_issue.assert_not_called()
39-
self.mock_gh.create_pr.assert_called_once_with("2.0.0", 123)
39+
self.mock_gh.create_pr.assert_called_once_with(
40+
title="Prepare release v2.0.0",
41+
body="Work towards #123",
42+
base="main",
43+
)
4044
self.mock_git.add_modified_and_deleted.assert_called_once()
4145

4246
@patch("tools.private.release.prepare.changelog_news")
@@ -67,7 +71,11 @@ def test_prepare_success_create_issue(self, mock_replace, mock_changelog):
6771
self.mock_gh.create_tracking_issue.assert_called_once_with(
6872
"2.0.0", "dummy template content"
6973
)
70-
self.mock_gh.create_pr.assert_called_once_with("2.0.0", 123)
74+
self.mock_gh.create_pr.assert_called_once_with(
75+
title="Prepare release v2.0.0",
76+
body="Work towards #123",
77+
base="main",
78+
)
7179
self.mock_git.add_modified_and_deleted.assert_called_once()
7280

7381
@patch("tools.private.release.prepare.changelog_news")
@@ -172,7 +180,11 @@ def test_prepare_create_pr_when_none_associated(self, mock_replace, mock_changel
172180
"origin", "prepare-2.0.0", set_upstream=True, force=True
173181
)
174182
self.mock_gh.get_open_pr.assert_called_once_with("prepare-2.0.0")
175-
self.mock_gh.create_pr.assert_called_once_with("2.0.0", 123)
183+
self.mock_gh.create_pr.assert_called_once_with(
184+
title="Prepare release v2.0.0",
185+
body="Work towards #123",
186+
base="main",
187+
)
176188
self.mock_gh.update_issue_body.assert_called_once()
177189
call_args = self.mock_gh.update_issue_body.call_args[0]
178190
self.assertIn("pr=#789", call_args[1])

0 commit comments

Comments
 (0)