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
41 changes: 41 additions & 0 deletions tests/tools/private/release/create_rc_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import os
import pathlib
import tempfile
Expand Down Expand Up @@ -212,6 +213,46 @@ def test_create_rc_with_finished_backports(self):
self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0")
self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0")

def test_create_rc_auto_add_task(self):
# Arrange
args = argparse.Namespace(issue=123, remote="my-remote")
self.mock_gh.get_issue_title.return_value = "Release 2.0.0"
self.mock_gh.get_issue_body.return_value = """
## Checklist
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12
- [ ] Tag Final
"""
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"]
self.mock_git.get_commit_sha.return_value = "1234567890"

# Act
result = CreateRc(args, self.mock_git, self.mock_gh).run()

# Assert
self.assertEqual(result, 0)
self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0")
self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1")

self.assertEqual(self.mock_gh.update_issue_body.call_count, 2)
call1_args = self.mock_gh.update_issue_body.call_args_list[0][0]
call2_args = self.mock_gh.update_issue_body.call_args_list[1][0]

self.assertEqual(call1_args[0], 123)
self.assertIn("- [ ] Tag RC1", call1_args[1])
self.assertIn(
"- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12\n- [ ]"
" Tag RC1\n- [ ] Tag Final",
call1_args[1].strip(),
)

self.assertEqual(call2_args[0], 123)
self.assertIn(
"- [x] Tag RC1 | status=done tag=2.0.0-rc1 commit= 12345678",
call2_args[1],
)
Comment thread
rickeylev marked this conversation as resolved.


if __name__ == "__main__":
unittest.main()
58 changes: 58 additions & 0 deletions tests/tools/private/release/process_backports_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,64 @@ def mock_resolve(items):
self.mock_git.commit.assert_not_called()
self.mock_git.push.assert_not_called()

@patch("tools.private.release.process_backports.datetime")
def test_process_backports_add_backports_and_auto_add_rc_task(self, mock_datetime):
mock_datetime.date.today.return_value = datetime.date(2026, 7, 1)
args = argparse.Namespace(
issue=123,
remote="origin",
dry_run=False,
add=[124],
triggering_comment=None,
)
self.mock_gh.get_issue_title.return_value = "Release 2.0.0"
self.mock_gh.get_issue_body.return_value = """
## Checklist
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12
- [ ] Tag Final

## Backports
"""
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"]
self.mock_git.get_commit_sha.return_value = "12345678"
self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"'

def mock_resolve(items):
for item in items:
if item.pr_ref == "#124":
item.commit = "abcdef12"
item.status = "done"
return items

self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve
self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"]

result = ProcessBackports(args, self.mock_git, self.mock_gh).run()

self.assertEqual(result, 0)

# update_issue_body should be called twice:
# 1. When adding backports and auto-adding Tag RC1 task.
# 2. When updating the backport status to done.
self.assertEqual(self.mock_gh.update_issue_body.call_count, 2)

call1_args = self.mock_gh.update_issue_body.call_args_list[0][0]
call2_args = self.mock_gh.update_issue_body.call_args_list[1][0]

self.assertEqual(call1_args[0], 123)
self.assertIn("- [ ] #124", call1_args[1])
self.assertIn("- [ ] Tag RC1", call1_args[1])
self.assertIn(
"- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12\n- [ ]"
" Tag RC1\n- [ ] Tag Final",
call1_args[1].strip(),
)

self.assertEqual(call2_args[0], 123)
self.assertIn("- [x] #124 | status=done rc=rc1 commit= 12345678", call2_args[1])


if __name__ == "__main__":
unittest.main()
25 changes: 12 additions & 13 deletions tools/private/release/create_rc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from tools.private.release.git import Git
from tools.private.release.release_issue import (
RELEASE_TITLE_RE,
add_rc_task_to_body,
parse_backports,
parse_checklist_state,
update_task_in_body,
Expand Down Expand Up @@ -77,19 +78,17 @@ def run(self) -> int:
# Precheck: next RC number must exist and be unchecked in the checklist
rc_tags = state.get("rc_tags", {})
if next_rc_num not in rc_tags:
print(
f"Error: Checklist is missing required task 'Tag RC{next_rc_num}'"
f" to cut {version}-rc{next_rc_num}."
)
return 1

target_rc_task = rc_tags[next_rc_num]
if target_rc_task.checked or target_rc_task.status == "done":
print(
f"Error: Task 'Tag RC{next_rc_num}' is already marked done in"
" the checklist."
)
return 1
print(f"Task 'Tag RC{next_rc_num}' not found in checklist. Adding it...")
body = add_rc_task_to_body(body, next_rc_num)
self.gh.update_issue_body(args.issue, body)
Comment thread
rickeylev marked this conversation as resolved.
else:
target_rc_task = rc_tags[next_rc_num]
if target_rc_task.checked or target_rc_task.status == "done":
print(
f"Error: Task 'Tag RC{next_rc_num}' is already marked done in"
" the checklist."
)
return 1

target_ref = f"{args.remote}/{branch_name}"
commit_sha = self.git.get_commit_sha(target_ref)
Expand Down
17 changes: 17 additions & 0 deletions tools/private/release/process_backports.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from tools.private.release.release_issue import (
RELEASE_TITLE_RE,
add_backports_to_body,
add_rc_task_to_body,
parse_backports,
parse_checklist_state,
update_task_in_body,
)
from tools.private.release.utils import (
Expand Down Expand Up @@ -207,6 +209,19 @@ def _run_internal(self) -> int:
print(f"Adding backports {args.add} to tracking issue #{args.issue}...")
try:
body = add_backports_to_body(body, args.add)
state = parse_checklist_state(body)
rc_tags = state.get("rc_tags", {})
has_pending_rc = any(
not task.checked and task.status != "done"
for task in rc_tags.values()
)
next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0
if not has_pending_rc:
print(
f"No pending RC task found. Adding 'Tag"
f" RC{next_rc_num}' to checklist..."
)
body = add_rc_task_to_body(body, next_rc_num)
except ValueError as e:
print(f"Error: {e}")
return 1
Expand All @@ -219,6 +234,8 @@ def _run_internal(self) -> int:
"[DRY RUN] Would update tracking issue checklist with new"
" backports."
)
if not has_pending_rc:
print(f"[DRY RUN] Would add 'Tag RC{next_rc_num}' to checklist.")

items = parse_backports(body)

Expand Down
29 changes: 29 additions & 0 deletions tools/private/release/release_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,32 @@ def add_backports_to_body(body: str, prs: list[int]) -> str:
# Replace the old section with the updated one
start, end = match.span(2)
return body[:start] + updated_section + body[end:]


def add_rc_task_to_body(body: str, rc_num: int) -> str:
"""Adds a new 'Tag RC<rc_num>' task to the checklist in the issue body."""
body = body.replace("\r\n", "\n")
lines = body.splitlines()

# Find the index of the last "Tag RC<M>" line
last_rc_idx = -1
for i, line in enumerate(lines):
parsed = parse_metadata_line(line)
if parsed and re.match(r"Tag RC\d+", parsed["name"], re.IGNORECASE):
last_rc_idx = i

if last_rc_idx == -1:
# If no RC task found (unexpected, but fallback to before "Tag Final")
for i, line in enumerate(lines):
parsed = parse_metadata_line(line)
if parsed and parsed["name"].lower() == "tag final":
last_rc_idx = i - 1
break

if last_rc_idx == -1:
raise ValueError("Could not find a place to insert the new RC task.")

new_task_line = f"- [ ] Tag RC{rc_num}"
lines.insert(last_rc_idx + 1, new_task_line)
Comment thread
rickeylev marked this conversation as resolved.

return "\n".join(lines)