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
30 changes: 30 additions & 0 deletions .github/workflows/on_pr_closed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,33 @@ jobs:
"$PR_NUMBER" \
--remote origin \
--no-dry-run
complete_sync_changelog:
if: |
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'type: sync-changelog')
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Bazel
uses: bazel-contrib/setup-bazel@0.19.0
with:
bazelisk-version: 1.20.0

- name: Complete Sync Changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
bazel run //tools/private/release -- complete-sync-changelog \
--pr "$PR_NUMBER"
9 changes: 9 additions & 0 deletions tests/tools/private/release/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ py_test(
],
)

py_test(
name = "complete_sync_changelog_test",
srcs = ["complete_sync_changelog_test.py"],
deps = [
":release_test_helper",
"//tools/private/release:release_lib",
],
)

py_test(
name = "create_rc_test",
srcs = ["create_rc_test.py"],
Expand Down
3 changes: 3 additions & 0 deletions tests/tools/private/release/add_backports_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def test_add_backports_explicit_issue(self):
self.assertIn("- [ ] #125", call_args[1])
# Should also auto-add Tag RC0
self.assertIn("- [ ] Tag RC0", call_args[1])
self.assertIn("- [ ] Sync Changelog #124", call_args[1])
self.assertIn("- [ ] Sync Changelog #125", call_args[1])

def test_add_backports_auto_discover_success(self):
args = argparse.Namespace(issue=None, prs=["124"])
Expand Down Expand Up @@ -98,6 +100,7 @@ def test_add_backports_no_auto_add_rc_if_pending(self):
self.assertNotIn("Tag RC1", call_args[1])
# Tag RC0 should still be there
self.assertIn("- [ ] Tag RC0", call_args[1])
self.assertIn("- [ ] Sync Changelog #124", call_args[1])


if __name__ == "__main__":
Expand Down
141 changes: 126 additions & 15 deletions tests/tools/private/release/changelog_news_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,9 @@ def test_update_changelog_with_news(self):

[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased

{#unreleased-removed}
### Removed
* Nothing removed.

{#unreleased-changed}
### Changed
* Nothing changed.

{#unreleased-fixed}
### Fixed
* Nothing fixed.

{#unreleased-added}
### Added
* Nothing added.
Unreleased changes are tracked as individual files in the [news/](./news)
directory, or view the [latest generated
changelog](https://rules-python.readthedocs.io/en/latest/changelog.html).

{#v2-0-2}
## [2.0.2] - 2026-05-14
Expand Down Expand Up @@ -408,6 +396,129 @@ def test_update_changelog_empty_news(self):
self.assertNotIn("{#v3-0-0-fixed}", new_content)
self.assertNotIn("{#v3-0-0-added}", new_content)

def test_update_changelog_selective_news_files(self):
# Arrange
changelog = """# Changelog

{#unreleased}
## Unreleased

[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased

{#v2-0-2}
## [2.0.2] - 2026-05-14

[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2
"""
changelog_path = self.tmpdir / "CHANGELOG.md"
changelog_path.write_text(changelog)

news_dir = self.tmpdir / "news"
news_dir.mkdir()

# Create news files
(news_dir / "123.fixed.md").write_text("Fix A")
(news_dir / "456.fixed.md").write_text("Fix B")

# Act: Only process 123.fixed.md
changelog_news.update_changelog(
"2.0.3",
"2026-06-16",
changelog_path=changelog_path,
news_dir=news_dir,
news_files=[news_dir / "123.fixed.md"],
)

# Assert
# 1. Only 123.fixed.md should be deleted
self.assertFalse((news_dir / "123.fixed.md").exists())
self.assertTrue((news_dir / "456.fixed.md").exists())

new_content = changelog_path.read_text()

# 2. Only Fix A should be in the changelog
self.assertIn("Fix A", new_content)
self.assertNotIn("Fix B", new_content)

def test_update_changelog_insertion_point(self):
# Arrange
changelog = """# Changelog

{#unreleased}
## Unreleased

[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased

{#v2-2-0}
## [2.2.0] - 2026-06-30

[2.2.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.2.0

{#v2-0-0}
## [2.0.0] - 2026-04-09

[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0
"""
changelog_path = self.tmpdir / "CHANGELOG.md"
changelog_path.write_text(changelog)

news_dir = self.tmpdir / "news"
news_dir.mkdir()
(news_dir / "123.fixed.md").write_text("Fix in 2.1.0")

# Act: Insert 2.1.0
changelog_news.update_changelog(
"2.1.0",
"2026-06-17",
changelog_path=changelog_path,
news_dir=news_dir,
)

# Assert
new_content = changelog_path.read_text()

# Verify 2.1.0 is inserted BEFORE 2.0.0 but AFTER 2.2.0
idx_2_2_0 = new_content.index("{#v2-2-0}")
idx_2_1_0 = new_content.index("{#v2-1-0}")
idx_2_0_0 = new_content.index("{#v2-0-0}")

self.assertTrue(idx_2_2_0 < idx_2_1_0 < idx_2_0_0)
self.assertIn("Fix in 2.1.0", new_content)

def test_update_changelog_insertion_point_too_small(self):
# Arrange
changelog = """# Changelog

{#unreleased}
## Unreleased

[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased

{#v2-0-0}
## [2.0.0] - 2026-04-09

[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0
"""
changelog_path = self.tmpdir / "CHANGELOG.md"
changelog_path.write_text(changelog)

news_dir = self.tmpdir / "news"
news_dir.mkdir()
(news_dir / "123.fixed.md").write_text("Fix in 1.0.0")

# Act & Assert
with self.assertRaises(ValueError) as ctx:
changelog_news.update_changelog(
"1.0.0",
"2026-01-01",
changelog_path=changelog_path,
news_dir=news_dir,
)
self.assertIn(
"Could not find a version in CHANGELOG.md smaller than 1.0.0",
str(ctx.exception),
)
Comment thread
rickeylev marked this conversation as resolved.


if __name__ == "__main__":
unittest.main()
120 changes: 120 additions & 0 deletions tests/tools/private/release/complete_sync_changelog_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import argparse
import unittest
from unittest.mock import patch

from tests.tools.private.release.release_test_helper import _mock_git_and_gh
from tools.private.release.complete_sync_changelog import CompleteSyncChangelog


class CompleteSyncChangelogTest(unittest.TestCase):
def setUp(self):
_mock_git_and_gh(self)
self.addCleanup(patch.stopall)

# Dynamic mock for issue body
self.issue_body = ""

def mock_get_body(issue_num):
return self.issue_body

def mock_update_body(issue_num, body):
self.issue_body = body

self.mock_gh.get_issue_body.side_effect = mock_get_body
self.mock_gh.update_issue_body.side_effect = mock_update_body

def test_complete_sync_changelog_success(self):
args = argparse.Namespace(pr=999)
self.mock_gh.get_pr_info.return_value = {
"state": "MERGED",
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
"mergeCommit": {"oid": "abcdef1234567890"},
}
self.issue_body = """
## Checklist
- [ ] Prepare Release
- [ ] Create Release branch
- [ ] Sync Changelog #124 | status=pending pr=#999
- [ ] Sync Changelog #125 | status=pending pr=#999
- [ ] Sync Changelog #126 | status=pending pr=#888
- [ ] Tag Final

## Backports
"""
result = CompleteSyncChangelog(args, self.mock_gh).run()

self.assertEqual(result, 0)
self.mock_gh.get_pr_info.assert_called_once_with(999)
self.mock_gh.get_issue_body.assert_called_once_with(123)
self.mock_gh.update_issue_body.assert_called_once()

# Check that only tasks pointing to #999 were marked checked=True and status=done
self.assertIn(
"- [x] Sync Changelog #124 | status=done pr=#999 commit= abcdef12",
self.issue_body,
)
self.assertIn(
"- [x] Sync Changelog #125 | status=done pr=#999 commit= abcdef12",
self.issue_body,
)
# Task pointing to #888 should remain unchanged
self.assertIn(
"- [ ] Sync Changelog #126 | status=pending pr=#888",
self.issue_body,
)

def test_complete_sync_changelog_not_merged(self):
args = argparse.Namespace(pr=999)
self.mock_gh.get_pr_info.return_value = {
"state": "OPEN",
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
}

result = CompleteSyncChangelog(args, self.mock_gh).run()

self.assertEqual(result, 1)
self.mock_gh.get_pr_info.assert_called_once_with(999)
self.mock_gh.update_issue_body.assert_not_called()

def test_complete_sync_changelog_missing_tracking_issue_link(self):
args = argparse.Namespace(pr=999)
self.mock_gh.get_pr_info.return_value = {
"state": "MERGED",
"body": "Updates CHANGELOG.md without tracking issue link",
"mergeCommit": {"oid": "abcdef1234567890"},
}

result = CompleteSyncChangelog(args, self.mock_gh).run()

self.assertEqual(result, 1)
self.mock_gh.get_pr_info.assert_called_once_with(999)
self.mock_gh.update_issue_body.assert_not_called()

def test_complete_sync_changelog_no_matching_tasks(self):
args = argparse.Namespace(pr=999)
self.mock_gh.get_pr_info.return_value = {
"state": "MERGED",
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
"mergeCommit": {"oid": "abcdef1234567890"},
}
# Checklist has no tasks pointing to #999
self.issue_body = """
## Checklist
- [ ] Prepare Release
- [ ] Create Release branch
- [ ] Sync Changelog #124 | status=pending pr=#888
- [ ] Tag Final

## Backports
"""
result = CompleteSyncChangelog(args, self.mock_gh).run()

# Should log warning but return 0 (success/noop)
self.assertEqual(result, 0)
self.mock_gh.get_pr_info.assert_called_once_with(999)
self.mock_gh.get_issue_body.assert_called_once_with(123)
self.mock_gh.update_issue_body.assert_not_called()


if __name__ == "__main__":
unittest.main()
Loading