Skip to content

Commit f6e66fe

Browse files
committed
feat(release): track Sync Changelog tasks in release issue
Add 'Sync Changelog' tasks to checklist when adding backports. Automatically mark them as done when the sync PR is merged using a new complete-sync-changelog subcommand and GitHub Actions workflow.
1 parent 24c1c10 commit f6e66fe

12 files changed

Lines changed: 543 additions & 34 deletions

.github/workflows/on_pr_closed.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,33 @@ jobs:
8181
"$PR_NUMBER" \
8282
--remote origin \
8383
--no-dry-run
84+
85+
complete_sync_changelog:
86+
if: |
87+
github.event.pull_request.merged == true &&
88+
contains(github.event.pull_request.labels.*.name, 'type: sync-changelog')
89+
runs-on: ubuntu-latest
90+
permissions:
91+
contents: write
92+
issues: write
93+
pull-requests: read
94+
steps:
95+
- name: Checkout repository
96+
uses: actions/checkout@v7
97+
with:
98+
fetch-depth: 0
99+
100+
- name: Setup Bazel
101+
uses: bazel-contrib/setup-bazel@0.19.0
102+
with:
103+
bazelisk-version: 1.20.0
104+
105+
- name: Complete Sync Changelog
106+
env:
107+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
108+
PR_NUMBER: ${{ github.event.pull_request.number }}
109+
run: |
110+
bazel run //tools/private/release -- complete-sync-changelog \
111+
--pr "$PR_NUMBER"
112+
113+

tests/tools/private/release/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ py_test(
2626
],
2727
)
2828

29+
py_test(
30+
name = "complete_sync_changelog_test",
31+
srcs = ["complete_sync_changelog_test.py"],
32+
deps = [
33+
":release_test_helper",
34+
"//tools/private/release:release_lib",
35+
],
36+
)
37+
2938
py_test(
3039
name = "create_rc_test",
3140
srcs = ["create_rc_test.py"],

tests/tools/private/release/add_backports_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def test_add_backports_explicit_issue(self):
3535
self.assertIn("- [ ] #125", call_args[1])
3636
# Should also auto-add Tag RC0
3737
self.assertIn("- [ ] Tag RC0", call_args[1])
38+
self.assertIn("- [ ] Sync Changelog #124", call_args[1])
39+
self.assertIn("- [ ] Sync Changelog #125", call_args[1])
3840

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

102105

103106
if __name__ == "__main__":
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import argparse
2+
import unittest
3+
from unittest.mock import patch
4+
5+
from tests.tools.private.release.release_test_helper import _mock_git_and_gh
6+
from tools.private.release.complete_sync_changelog import CompleteSyncChangelog
7+
8+
9+
class CompleteSyncChangelogTest(unittest.TestCase):
10+
def setUp(self):
11+
_mock_git_and_gh(self)
12+
self.addCleanup(patch.stopall)
13+
14+
# Dynamic mock for issue body
15+
self.issue_body = ""
16+
17+
def mock_get_body(issue_num):
18+
return self.issue_body
19+
20+
def mock_update_body(issue_num, body):
21+
self.issue_body = body
22+
23+
self.mock_gh.get_issue_body.side_effect = mock_get_body
24+
self.mock_gh.update_issue_body.side_effect = mock_update_body
25+
26+
def test_complete_sync_changelog_success(self):
27+
args = argparse.Namespace(pr=999)
28+
self.mock_gh.get_pr_info.return_value = {
29+
"state": "MERGED",
30+
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
31+
"mergeCommit": {"oid": "abcdef1234567890"},
32+
}
33+
self.issue_body = """
34+
## Checklist
35+
- [ ] Prepare Release
36+
- [ ] Create Release branch
37+
- [ ] Sync Changelog #124 | status=pending pr=#999
38+
- [ ] Sync Changelog #125 | status=pending pr=#999
39+
- [ ] Sync Changelog #126 | status=pending pr=#888
40+
- [ ] Tag Final
41+
42+
## Backports
43+
"""
44+
result = CompleteSyncChangelog(args, self.mock_gh).run()
45+
46+
self.assertEqual(result, 0)
47+
self.mock_gh.get_pr_info.assert_called_once_with(999)
48+
self.mock_gh.get_issue_body.assert_called_once_with(123)
49+
self.mock_gh.update_issue_body.assert_called_once()
50+
51+
# Check that only tasks pointing to #999 were marked checked=True and status=done
52+
self.assertIn(
53+
"- [x] Sync Changelog #124 | status=done pr=#999 commit= abcdef12",
54+
self.issue_body,
55+
)
56+
self.assertIn(
57+
"- [x] Sync Changelog #125 | status=done pr=#999 commit= abcdef12",
58+
self.issue_body,
59+
)
60+
# Task pointing to #888 should remain unchanged
61+
self.assertIn(
62+
"- [ ] Sync Changelog #126 | status=pending pr=#888",
63+
self.issue_body,
64+
)
65+
66+
def test_complete_sync_changelog_not_merged(self):
67+
args = argparse.Namespace(pr=999)
68+
self.mock_gh.get_pr_info.return_value = {
69+
"state": "OPEN",
70+
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
71+
}
72+
73+
result = CompleteSyncChangelog(args, self.mock_gh).run()
74+
75+
self.assertEqual(result, 1)
76+
self.mock_gh.get_pr_info.assert_called_once_with(999)
77+
self.mock_gh.update_issue_body.assert_not_called()
78+
79+
def test_complete_sync_changelog_missing_tracking_issue_link(self):
80+
args = argparse.Namespace(pr=999)
81+
self.mock_gh.get_pr_info.return_value = {
82+
"state": "MERGED",
83+
"body": "Updates CHANGELOG.md without tracking issue link",
84+
"mergeCommit": {"oid": "abcdef1234567890"},
85+
}
86+
87+
result = CompleteSyncChangelog(args, self.mock_gh).run()
88+
89+
self.assertEqual(result, 1)
90+
self.mock_gh.get_pr_info.assert_called_once_with(999)
91+
self.mock_gh.update_issue_body.assert_not_called()
92+
93+
def test_complete_sync_changelog_no_matching_tasks(self):
94+
args = argparse.Namespace(pr=999)
95+
self.mock_gh.get_pr_info.return_value = {
96+
"state": "MERGED",
97+
"body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123",
98+
"mergeCommit": {"oid": "abcdef1234567890"},
99+
}
100+
# Checklist has no tasks pointing to #999
101+
self.issue_body = """
102+
## Checklist
103+
- [ ] Prepare Release
104+
- [ ] Create Release branch
105+
- [ ] Sync Changelog #124 | status=pending pr=#888
106+
- [ ] Tag Final
107+
108+
## Backports
109+
"""
110+
result = CompleteSyncChangelog(args, self.mock_gh).run()
111+
112+
# Should log warning but return 0 (success/noop)
113+
self.assertEqual(result, 0)
114+
self.mock_gh.get_pr_info.assert_called_once_with(999)
115+
self.mock_gh.get_issue_body.assert_called_once_with(123)
116+
self.mock_gh.update_issue_body.assert_not_called()
117+
118+
119+
if __name__ == "__main__":
120+
unittest.main()

0 commit comments

Comments
 (0)