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
3 changes: 1 addition & 2 deletions .github/workflows/check_version_markers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ grep_exit_code=0
# Exclude CONTRIBUTING.md, RELEASING.md because they document how to use these strings.
grep --exclude=CONTRIBUTING.md \
--exclude=RELEASING.md \
--exclude=release.py \
--exclude=release_test.py \
--exclude-dir=.* \
--exclude-dir=release \
VERSION_NEXT_ -r || grep_exit_code=$?

if [[ $grep_exit_code -eq 0 ]]; then
Expand Down
72 changes: 32 additions & 40 deletions tests/tools/private/release/release_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _mock_git_and_gh(test_case):
# Apply safe defaults
mock_git.get_current_branch.return_value = None
mock_git.get_tags.return_value = []
mock_git.get_tags_at_head.return_value = []
mock_git.get_remote_tags.return_value = []

mock_git.status.return_value = ""
mock_git.branch_exists.return_value = False
mock_git.tag_exists.return_value = False
Expand Down Expand Up @@ -588,6 +589,17 @@ def test_get_latest_rc_tag_ignores_v_prefix(self, mock_get_tags):
mock_get_tags.return_value = ["v2.0.0-rc0", "2.0.0-rc1"]
self.assertEqual(utils.get_latest_rc_tag("2.0.0"), "2.0.0-rc1")

@patch("tools.private.release.git.get_remote_tags")
def test_get_latest_rc_tag_remote_success(self, mock_get_remote_tags):
mock_get_remote_tags.return_value = [
"2.0.0-rc0",
"2.0.0-rc2",
"2.0.0-rc1",
"2.1.0-rc0",
]
self.assertEqual(utils.get_latest_rc_tag("2.0.0", remote="origin"), "2.0.0-rc2")
mock_get_remote_tags.assert_called_once_with("origin")


class DetermineNextVersionTest(TempDirTestCase):
def setUp(self):
Expand Down Expand Up @@ -942,8 +954,7 @@ def test_create_rc_success_first_rc(self):
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
- [ ] Tag RC0 | status=pending
"""
self.mock_git.get_tags.return_value = []
self.mock_git.get_tags_at_head.return_value = []
self.mock_git.get_remote_tags.return_value = []
self.mock_git.get_commit_sha.return_value = "1234567890"

# Act
Expand All @@ -954,9 +965,10 @@ def test_create_rc_success_first_rc(self):
self.mock_git.fetch.assert_has_calls(
[call("my-remote"), call("my-remote", tags=True, force=True)]
)
self.mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "HEAD")
self.mock_git.checkout.assert_not_called()
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")
self.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")

self.mock_gh.update_issue_body.assert_called_once()
call_args = self.mock_gh.update_issue_body.call_args[0]
Expand Down Expand Up @@ -988,8 +1000,7 @@ def test_create_rc_success_next_rc(self):
- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12
- [ ] Tag RC1 | status=pending
"""
self.mock_git.get_tags.return_value = ["2.0.0-rc0"]
self.mock_git.get_tags_at_head.return_value = []
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"]
self.mock_git.get_commit_sha.return_value = "1234567890"

# Act
Expand All @@ -1000,9 +1011,10 @@ def test_create_rc_success_next_rc(self):
self.mock_git.fetch.assert_has_calls(
[call("my-remote"), call("my-remote", tags=True, force=True)]
)
self.mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "HEAD")
self.mock_git.checkout.assert_not_called()
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.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")

self.mock_gh.update_issue_body.assert_called_once()
call_args = self.mock_gh.update_issue_body.call_args[0]
Expand All @@ -1022,28 +1034,6 @@ def test_create_rc_success_next_rc(self):
)
self.assertNotIn("🚀", comment_call_args[1])

def test_create_rc_already_tagged(self):
# Arrange
args = MagicMock(issue=123)
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
- [ ] Tag RC0 | status=pending
"""
self.mock_git.get_tags.return_value = []
self.mock_git.get_tags_at_head.return_value = ["2.0.0-rc0"]

# Act
result = releaser.cmd_create_rc(args)

# Assert
self.assertEqual(result, 0)
self.mock_git.tag.assert_not_called()
self.mock_git.push.assert_not_called()
self.mock_gh.update_issue_body.assert_not_called()


class CmdPromoteRcTest(unittest.TestCase):
def setUp(self):
Expand All @@ -1052,7 +1042,7 @@ def setUp(self):
def test_promote_rc_success(self):
# Arrange
args = MagicMock(version="2.0.0", issue=123, dry_run=False)
self.mock_git.get_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
self.mock_git.get_commit_sha.return_value = "abcdef123456"
self.mock_git.tag_exists.return_value = False
initial_body = "- [ ] Tag Final"
Expand Down Expand Up @@ -1088,7 +1078,7 @@ def test_promote_rc_success(self):
def test_promote_rc_resolve_issue_success(self):
# Arrange
args = MagicMock(version="2.0.0", issue=None, dry_run=False)
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
self.mock_git.tag_exists.return_value = False
self.mock_gh.get_release_tracking_issue.side_effect = None
self.mock_gh.get_release_tracking_issue.return_value = 123
Expand Down Expand Up @@ -1124,7 +1114,8 @@ def test_promote_rc_defaults_to_determine_next_version(self):
# Arrange
args = MagicMock(version=None, issue=123, dry_run=False)
self.mock_git.get_current_branch.return_value = "release/2.0"
self.mock_git.get_tags.return_value = ["2.0.0", "2.0.1-rc0"]
self.mock_git.get_tags.return_value = ["2.0.0"]
self.mock_git.get_remote_tags.return_value = ["2.0.1-rc0"]
self.mock_git.get_commit_sha.return_value = "12345678"
self.mock_git.tag_exists.return_value = False
initial_body = "- [ ] Tag Final"
Expand All @@ -1136,7 +1127,8 @@ def test_promote_rc_defaults_to_determine_next_version(self):
# Assert
self.assertEqual(result, 0)
self.mock_git.get_current_branch.assert_called_once()
self.assertTrue(self.mock_git.get_tags.call_count >= 2)
self.mock_git.get_tags.assert_called_once()
self.mock_git.get_remote_tags.assert_called_once_with("upstream")

self.mock_git.checkout.assert_not_called()
self.mock_git.get_commit_sha.assert_called_once_with("2.0.1-rc0")
Expand All @@ -1159,7 +1151,7 @@ def test_promote_rc_defaults_to_determine_next_version(self):
def test_promote_rc_dry_run_success(self):
# Arrange
args = MagicMock(version="2.0.0", issue=123, dry_run=True)
self.mock_git.get_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
self.mock_git.get_commit_sha.return_value = "abcdef123456"
self.mock_git.tag_exists.return_value = False
initial_body = "- [ ] Tag Final"
Expand All @@ -1183,7 +1175,7 @@ def test_promote_rc_dry_run_success(self):
def test_promote_rc_tag_already_exists(self):
# Arrange
args = MagicMock(version="2.0.0", issue=123)
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
self.mock_git.tag_exists.return_value = True

# Act
Expand All @@ -1200,7 +1192,7 @@ def test_promote_rc_tag_already_exists(self):
def test_promote_rc_issue_not_found(self):
# Arrange
args = MagicMock(version="2.0.0", issue=None)
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
self.mock_git.tag_exists.return_value = False
self.mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError(
"Not found"
Expand All @@ -1220,7 +1212,7 @@ def test_promote_rc_issue_not_found(self):
def test_promote_rc_issue_malformed(self):
# Arrange
args = MagicMock(version="2.0.0", issue=123)
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
self.mock_git.tag_exists.return_value = False
self.mock_git.get_commit_sha.return_value = "abcdef123456"
initial_body = "malformed body"
Expand All @@ -1240,7 +1232,7 @@ def test_promote_rc_issue_malformed(self):
def test_promote_rc_no_rc_found(self):
# Arrange
args = MagicMock(version="2.0.0", issue=123)
self.mock_git.get_tags.return_value = []
self.mock_git.get_remote_tags.return_value = []

# Act
result = releaser.cmd_promote_rc(args)
Expand Down
14 changes: 4 additions & 10 deletions tools/private/release/create_rc.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def cmd_create_rc(args):
# Determine next RC tag
git.fetch(args.remote)
git.fetch(args.remote, tags=True, force=True)
latest_rc = get_latest_rc_tag(version)
latest_rc = get_latest_rc_tag(version, remote=args.remote)

if not latest_rc:
next_rc_num = 0
Expand All @@ -79,19 +79,13 @@ def cmd_create_rc(args):
)
return 1

# Verify HEAD is not already tagged
git.checkout(f"{args.remote}/{branch_name}")
head_tags = git.get_tags_at_head()
if any(tag.startswith(f"{version}-rc") for tag in head_tags):
print(f"HEAD of {branch_name} is already tagged with an RC. Skipping.")
return 0
target_ref = f"{args.remote}/{branch_name}"
commit_sha = git.get_commit_sha(target_ref)

print(f"Tagging and pushing next RC: {next_rc}...")
git.tag(next_rc, "HEAD")
git.tag(next_rc, target_ref)
git.push(args.remote, next_rc)

commit_sha = git.get_commit_sha("HEAD")

# Check off the appropriate "Tag RC{N}" task in the checklist
print(f"Checking off Tag RC{next_rc_num} task...")
metadata = {"status": "done", "tag": next_rc, "commit": commit_sha[:8]}
Expand Down
33 changes: 27 additions & 6 deletions tools/private/release/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,6 @@ def sort_commits_chronologically(shas):
return output.splitlines() if output else []


def get_tags_at_head():
"""Returns a list of tags pointing at the current HEAD commit."""
output = run_cmd("git", "tag", "--points-at", "HEAD")
return output.splitlines() if output else []


def get_current_branch():
"""Returns the current git branch name."""
return run_cmd("git", "rev-parse", "--abbrev-ref", "HEAD")
Expand All @@ -150,3 +144,30 @@ def is_ancestor(ancestor, descendant):
return True
except subprocess.CalledProcessError:
return False


def get_remote_tags(remote: str) -> list[str]:
"""Returns a list of tags present on the specified remote repository.

Args:
remote: The name of the git remote to query (e.g., 'origin', 'upstream').

Returns:
A list of tag names (strings) found on the remote, excluding peeled tags.
"""
output = run_cmd("git", "ls-remote", "--tags", remote)
tags = []
for line in output.splitlines():
Comment thread
rickeylev marked this conversation as resolved.
if not line:
continue
parts = line.split()
if len(parts) < 2:
continue
ref = parts[1]
if ref.startswith("refs/tags/"):
tag = ref[len("refs/tags/") :]
# Skip peeled tags (e.g. tag^{}) to avoid
# duplicate tag names in the output.
if not tag.endswith("^{}"):
tags.append(tag)
return tags
11 changes: 6 additions & 5 deletions tools/private/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ def cmd_process_backports(args):
branch_name = f"release/{branch_version}"

# Determine next RC tag to write to backport metadata
git.fetch("--tags", "--force")
latest_rc = get_latest_rc_tag(version)
git.fetch("origin", tags=True, force=True)
latest_rc = get_latest_rc_tag(version, remote="origin")
if not latest_rc:
next_rc_suffix = "rc0"
else:
Expand Down Expand Up @@ -236,13 +236,14 @@ def cmd_process_backports(args):

def cmd_promote_rc(args):
"""Executes the promote-rc subcommand (Phase 3)."""
# Fetch from upstream to ensure we have the latest tags
git.fetch("upstream", tags=True, force=True)

version = args.version
if version is None:
version = determine_next_version()

# Fetch from upstream to ensure we have the latest tags
git.fetch("upstream", tags=True, force=True)
latest_rc = get_latest_rc_tag(version)
latest_rc = get_latest_rc_tag(version, remote="upstream")
Comment thread
rickeylev marked this conversation as resolved.
if not latest_rc:
print(f"Error: No release candidate tags found matching {version}-rc*")
return 1
Expand Down
7 changes: 5 additions & 2 deletions tools/private/release/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ def get_latest_version():
return stable_versions[-1]


def get_latest_rc_tag(version):
def get_latest_rc_tag(version, remote=None):
"""Queries git tags and returns the highest RC tag for the version."""
tags = git.get_tags()
if remote:
tags = git.get_remote_tags(remote)
else:
tags = git.get_tags()
pattern = rf"^{re.escape(version)}-rc\d+$"
rc_tags = [tag.strip() for tag in tags if re.match(pattern, tag.strip())]
if not rc_tags:
Expand Down