Skip to content

Commit dc0d977

Browse files
authored
chore(release): various fixes to release tool and workflows (bazel-contrib#3880)
The release tool previously calculated incorrect RC versions or failed when local tags were out of sync with the remote repository. To resolve this: - Query remote tags directly via `git ls-remote` to determine the next RC version. - Fetch upstream tags before determining the next version in `promote-rc` to avoid stale local state. - Remove local checkout and the "already tagged" check in `create-rc` to simplify the tagging process. - Exclude release tool directories from the workflow version marker check.
1 parent 2241f8b commit dc0d977

6 files changed

Lines changed: 75 additions & 65 deletions

File tree

.github/workflows/check_version_markers.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ grep_exit_code=0
2020
# Exclude CONTRIBUTING.md, RELEASING.md because they document how to use these strings.
2121
grep --exclude=CONTRIBUTING.md \
2222
--exclude=RELEASING.md \
23-
--exclude=release.py \
24-
--exclude=release_test.py \
2523
--exclude-dir=.* \
24+
--exclude-dir=release \
2625
VERSION_NEXT_ -r || grep_exit_code=$?
2726

2827
if [[ $grep_exit_code -eq 0 ]]; then

tests/tools/private/release/release_test.py

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def _mock_git_and_gh(test_case):
3434
# Apply safe defaults
3535
mock_git.get_current_branch.return_value = None
3636
mock_git.get_tags.return_value = []
37-
mock_git.get_tags_at_head.return_value = []
37+
mock_git.get_remote_tags.return_value = []
38+
3839
mock_git.status.return_value = ""
3940
mock_git.branch_exists.return_value = False
4041
mock_git.tag_exists.return_value = False
@@ -588,6 +589,17 @@ def test_get_latest_rc_tag_ignores_v_prefix(self, mock_get_tags):
588589
mock_get_tags.return_value = ["v2.0.0-rc0", "2.0.0-rc1"]
589590
self.assertEqual(utils.get_latest_rc_tag("2.0.0"), "2.0.0-rc1")
590591

592+
@patch("tools.private.release.git.get_remote_tags")
593+
def test_get_latest_rc_tag_remote_success(self, mock_get_remote_tags):
594+
mock_get_remote_tags.return_value = [
595+
"2.0.0-rc0",
596+
"2.0.0-rc2",
597+
"2.0.0-rc1",
598+
"2.1.0-rc0",
599+
]
600+
self.assertEqual(utils.get_latest_rc_tag("2.0.0", remote="origin"), "2.0.0-rc2")
601+
mock_get_remote_tags.assert_called_once_with("origin")
602+
591603

592604
class DetermineNextVersionTest(TempDirTestCase):
593605
def setUp(self):
@@ -942,8 +954,7 @@ def test_create_rc_success_first_rc(self):
942954
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
943955
- [ ] Tag RC0 | status=pending
944956
"""
945-
self.mock_git.get_tags.return_value = []
946-
self.mock_git.get_tags_at_head.return_value = []
957+
self.mock_git.get_remote_tags.return_value = []
947958
self.mock_git.get_commit_sha.return_value = "1234567890"
948959

949960
# Act
@@ -954,9 +965,10 @@ def test_create_rc_success_first_rc(self):
954965
self.mock_git.fetch.assert_has_calls(
955966
[call("my-remote"), call("my-remote", tags=True, force=True)]
956967
)
957-
self.mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
958-
self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "HEAD")
968+
self.mock_git.checkout.assert_not_called()
969+
self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0")
959970
self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0")
971+
self.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")
960972

961973
self.mock_gh.update_issue_body.assert_called_once()
962974
call_args = self.mock_gh.update_issue_body.call_args[0]
@@ -988,8 +1000,7 @@ def test_create_rc_success_next_rc(self):
9881000
- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12
9891001
- [ ] Tag RC1 | status=pending
9901002
"""
991-
self.mock_git.get_tags.return_value = ["2.0.0-rc0"]
992-
self.mock_git.get_tags_at_head.return_value = []
1003+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"]
9931004
self.mock_git.get_commit_sha.return_value = "1234567890"
9941005

9951006
# Act
@@ -1000,9 +1011,10 @@ def test_create_rc_success_next_rc(self):
10001011
self.mock_git.fetch.assert_has_calls(
10011012
[call("my-remote"), call("my-remote", tags=True, force=True)]
10021013
)
1003-
self.mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
1004-
self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "HEAD")
1014+
self.mock_git.checkout.assert_not_called()
1015+
self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0")
10051016
self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1")
1017+
self.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")
10061018

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

1025-
def test_create_rc_already_tagged(self):
1026-
# Arrange
1027-
args = MagicMock(issue=123)
1028-
self.mock_gh.get_issue_title.return_value = "Release 2.0.0"
1029-
self.mock_gh.get_issue_body.return_value = """
1030-
## Checklist
1031-
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
1032-
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
1033-
- [ ] Tag RC0 | status=pending
1034-
"""
1035-
self.mock_git.get_tags.return_value = []
1036-
self.mock_git.get_tags_at_head.return_value = ["2.0.0-rc0"]
1037-
1038-
# Act
1039-
result = releaser.cmd_create_rc(args)
1040-
1041-
# Assert
1042-
self.assertEqual(result, 0)
1043-
self.mock_git.tag.assert_not_called()
1044-
self.mock_git.push.assert_not_called()
1045-
self.mock_gh.update_issue_body.assert_not_called()
1046-
10471037

10481038
class CmdPromoteRcTest(unittest.TestCase):
10491039
def setUp(self):
@@ -1052,7 +1042,7 @@ def setUp(self):
10521042
def test_promote_rc_success(self):
10531043
# Arrange
10541044
args = MagicMock(version="2.0.0", issue=123, dry_run=False)
1055-
self.mock_git.get_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
1045+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
10561046
self.mock_git.get_commit_sha.return_value = "abcdef123456"
10571047
self.mock_git.tag_exists.return_value = False
10581048
initial_body = "- [ ] Tag Final"
@@ -1088,7 +1078,7 @@ def test_promote_rc_success(self):
10881078
def test_promote_rc_resolve_issue_success(self):
10891079
# Arrange
10901080
args = MagicMock(version="2.0.0", issue=None, dry_run=False)
1091-
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
1081+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
10921082
self.mock_git.tag_exists.return_value = False
10931083
self.mock_gh.get_release_tracking_issue.side_effect = None
10941084
self.mock_gh.get_release_tracking_issue.return_value = 123
@@ -1124,7 +1114,8 @@ def test_promote_rc_defaults_to_determine_next_version(self):
11241114
# Arrange
11251115
args = MagicMock(version=None, issue=123, dry_run=False)
11261116
self.mock_git.get_current_branch.return_value = "release/2.0"
1127-
self.mock_git.get_tags.return_value = ["2.0.0", "2.0.1-rc0"]
1117+
self.mock_git.get_tags.return_value = ["2.0.0"]
1118+
self.mock_git.get_remote_tags.return_value = ["2.0.1-rc0"]
11281119
self.mock_git.get_commit_sha.return_value = "12345678"
11291120
self.mock_git.tag_exists.return_value = False
11301121
initial_body = "- [ ] Tag Final"
@@ -1136,7 +1127,8 @@ def test_promote_rc_defaults_to_determine_next_version(self):
11361127
# Assert
11371128
self.assertEqual(result, 0)
11381129
self.mock_git.get_current_branch.assert_called_once()
1139-
self.assertTrue(self.mock_git.get_tags.call_count >= 2)
1130+
self.mock_git.get_tags.assert_called_once()
1131+
self.mock_git.get_remote_tags.assert_called_once_with("upstream")
11401132

11411133
self.mock_git.checkout.assert_not_called()
11421134
self.mock_git.get_commit_sha.assert_called_once_with("2.0.1-rc0")
@@ -1159,7 +1151,7 @@ def test_promote_rc_defaults_to_determine_next_version(self):
11591151
def test_promote_rc_dry_run_success(self):
11601152
# Arrange
11611153
args = MagicMock(version="2.0.0", issue=123, dry_run=True)
1162-
self.mock_git.get_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
1154+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"]
11631155
self.mock_git.get_commit_sha.return_value = "abcdef123456"
11641156
self.mock_git.tag_exists.return_value = False
11651157
initial_body = "- [ ] Tag Final"
@@ -1183,7 +1175,7 @@ def test_promote_rc_dry_run_success(self):
11831175
def test_promote_rc_tag_already_exists(self):
11841176
# Arrange
11851177
args = MagicMock(version="2.0.0", issue=123)
1186-
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
1178+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
11871179
self.mock_git.tag_exists.return_value = True
11881180

11891181
# Act
@@ -1200,7 +1192,7 @@ def test_promote_rc_tag_already_exists(self):
12001192
def test_promote_rc_issue_not_found(self):
12011193
# Arrange
12021194
args = MagicMock(version="2.0.0", issue=None)
1203-
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
1195+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
12041196
self.mock_git.tag_exists.return_value = False
12051197
self.mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError(
12061198
"Not found"
@@ -1220,7 +1212,7 @@ def test_promote_rc_issue_not_found(self):
12201212
def test_promote_rc_issue_malformed(self):
12211213
# Arrange
12221214
args = MagicMock(version="2.0.0", issue=123)
1223-
self.mock_git.get_tags.return_value = ["2.0.0-rc1"]
1215+
self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"]
12241216
self.mock_git.tag_exists.return_value = False
12251217
self.mock_git.get_commit_sha.return_value = "abcdef123456"
12261218
initial_body = "malformed body"
@@ -1240,7 +1232,7 @@ def test_promote_rc_issue_malformed(self):
12401232
def test_promote_rc_no_rc_found(self):
12411233
# Arrange
12421234
args = MagicMock(version="2.0.0", issue=123)
1243-
self.mock_git.get_tags.return_value = []
1235+
self.mock_git.get_remote_tags.return_value = []
12441236

12451237
# Act
12461238
result = releaser.cmd_promote_rc(args)

tools/private/release/create_rc.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def cmd_create_rc(args):
5353
# Determine next RC tag
5454
git.fetch(args.remote)
5555
git.fetch(args.remote, tags=True, force=True)
56-
latest_rc = get_latest_rc_tag(version)
56+
latest_rc = get_latest_rc_tag(version, remote=args.remote)
5757

5858
if not latest_rc:
5959
next_rc_num = 0
@@ -79,19 +79,13 @@ def cmd_create_rc(args):
7979
)
8080
return 1
8181

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

8985
print(f"Tagging and pushing next RC: {next_rc}...")
90-
git.tag(next_rc, "HEAD")
86+
git.tag(next_rc, target_ref)
9187
git.push(args.remote, next_rc)
9288

93-
commit_sha = git.get_commit_sha("HEAD")
94-
9589
# Check off the appropriate "Tag RC{N}" task in the checklist
9690
print(f"Checking off Tag RC{next_rc_num} task...")
9791
metadata = {"status": "done", "tag": next_rc, "commit": commit_sha[:8]}

tools/private/release/git.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,6 @@ def sort_commits_chronologically(shas):
123123
return output.splitlines() if output else []
124124

125125

126-
def get_tags_at_head():
127-
"""Returns a list of tags pointing at the current HEAD commit."""
128-
output = run_cmd("git", "tag", "--points-at", "HEAD")
129-
return output.splitlines() if output else []
130-
131-
132126
def get_current_branch():
133127
"""Returns the current git branch name."""
134128
return run_cmd("git", "rev-parse", "--abbrev-ref", "HEAD")
@@ -150,3 +144,30 @@ def is_ancestor(ancestor, descendant):
150144
return True
151145
except subprocess.CalledProcessError:
152146
return False
147+
148+
149+
def get_remote_tags(remote: str) -> list[str]:
150+
"""Returns a list of tags present on the specified remote repository.
151+
152+
Args:
153+
remote: The name of the git remote to query (e.g., 'origin', 'upstream').
154+
155+
Returns:
156+
A list of tag names (strings) found on the remote, excluding peeled tags.
157+
"""
158+
output = run_cmd("git", "ls-remote", "--tags", remote)
159+
tags = []
160+
for line in output.splitlines():
161+
if not line:
162+
continue
163+
parts = line.split()
164+
if len(parts) < 2:
165+
continue
166+
ref = parts[1]
167+
if ref.startswith("refs/tags/"):
168+
tag = ref[len("refs/tags/") :]
169+
# Skip peeled tags (e.g. tag^{}) to avoid
170+
# duplicate tag names in the output.
171+
if not tag.endswith("^{}"):
172+
tags.append(tag)
173+
return tags

tools/private/release/release.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ def cmd_process_backports(args):
141141
branch_name = f"release/{branch_version}"
142142

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

237237
def cmd_promote_rc(args):
238238
"""Executes the promote-rc subcommand (Phase 3)."""
239+
# Fetch from upstream to ensure we have the latest tags
240+
git.fetch("upstream", tags=True, force=True)
241+
239242
version = args.version
240243
if version is None:
241244
version = determine_next_version()
242245

243-
# Fetch from upstream to ensure we have the latest tags
244-
git.fetch("upstream", tags=True, force=True)
245-
latest_rc = get_latest_rc_tag(version)
246+
latest_rc = get_latest_rc_tag(version, remote="upstream")
246247
if not latest_rc:
247248
print(f"Error: No release candidate tags found matching {version}-rc*")
248249
return 1

tools/private/release/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ def get_latest_version():
6767
return stable_versions[-1]
6868

6969

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

0 commit comments

Comments
 (0)