Skip to content

Commit 17febd7

Browse files
committed
Allow-list the release candidate's RC and GA tags for runner groups
Release binaries are built from tags, not branches: pushing v2.14.0-rc1 runs the build workflows at refs/tags/v2.14.0-rc1. GitHub matches selected_workflows entries on the exact ref, so the @refs/heads/release/2.14 entries this script already emits do not authorize those runs, and the tag had to be added to the groups by hand for 2.14.0-rc1. Emit the candidate version's tag refs alongside the branch refs. Only the GA tag and the newest v<version>-rc<n> are pinned; each RC supersedes the last, and pinning every one would grow both the allow-list and the per-ref discovery cost by a workflow set per tag (2.13 reached rc15). A newer RC evicting an older one is fine - the allow-list is checked when a job starts, so in-flight builds are unaffected. Tags come from git/matching-refs, which returns the refs under the prefix in one request rather than paging pytorch/pytorch's entire tag history. The prefix is not anchored (v2.1 also matches v2.1.0), so names are still filtered against the exact tag pattern.
1 parent 00fa578 commit 17febd7

2 files changed

Lines changed: 108 additions & 6 deletions

File tree

tools/scripts/release_manage_runner_groups.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
88
- ensure ``pytorch/pytorch`` is an allowed repository (add-only), and
99
- restrict allowed workflows to the release workflows discovered in
10-
``pytorch/pytorch``, pinned to ``main``, ``nightly`` and the release branches
10+
``pytorch/pytorch``, pinned to ``main``, ``nightly``, the release branches
1111
around the test-channel version (the ``release/X.Y`` anchor read from
1212
``generate_binary_build_matrix.py`` plus the preceding protected release
13-
branch).
13+
branch), and the candidate's release tags (the GA ``v<version>`` tag and the
14+
newest ``v<version>-rc<n>``, which is where the release binaries are actually
15+
built from).
1416
1517
Release workflows are discovered, not hardcoded, by the release runner label
1618
they run on (the ``rel-`` marker, e.g. ``rel-l-x86iavx512-44-340``). An entry
@@ -111,15 +113,19 @@ def paginate(
111113
# --- Desired state: target refs -------------------------------------------
112114

113115

114-
def get_test_version_anchor() -> Tuple[int, int]:
116+
def get_candidate_version() -> str:
115117
# The release runner groups serve the release-candidate builds, so anchor on
116118
# CURRENT_CANDIDATE_VERSION from generate_binary_build_matrix (the version
117119
# used for release builds, advanced deliberately at go-live) rather than
118120
# inferring it from a branch-name scan (which drifts: a release/X.Y branch is
119121
# cut weeks before it is the actual candidate).
120122
import generate_binary_build_matrix as gbm
121123

122-
major, minor = gbm.CURRENT_CANDIDATE_VERSION.split(".")[:2]
124+
return str(gbm.CURRENT_CANDIDATE_VERSION)
125+
126+
127+
def get_test_version_anchor() -> Tuple[int, int]:
128+
major, minor = get_candidate_version().split(".")[:2]
123129
return int(major), int(minor)
124130

125131

@@ -145,13 +151,71 @@ def select_target_refs(
145151
return [f"refs/heads/{name}" for name in selected]
146152

147153

154+
def release_tag_re(version: str) -> "re.Pattern[str]":
155+
"""Matches the GA and release-candidate tags for ``version``.
156+
157+
pytorch/pytorch tags releases as ``v<version>`` with candidates numbered
158+
``v<version>-rc<n>``, e.g. ``v2.14.0`` and ``v2.14.0-rc1``.
159+
"""
160+
return re.compile(rf"^v{re.escape(version)}(?:-rc(\d+))?$")
161+
162+
163+
def select_target_tags(tag_names: Iterable[str], version: str) -> List[str]:
164+
"""The GA tag and the newest release-candidate tag for ``version``.
165+
166+
Release binaries are built from tags, not branches: pushing ``v2.14.0-rc1``
167+
runs the build workflows at ``refs/tags/v2.14.0-rc1``. GitHub matches
168+
``selected_workflows`` entries on the exact ref, so a
169+
``@refs/heads/release/2.14`` entry does not authorize that run and the tag
170+
has to be pinned in its own right.
171+
172+
Only the newest RC is kept: each tag supersedes the last, and pinning every
173+
one would grow both the allow-list and the per-ref discovery cost by a
174+
workflow set per tag (2.13 reached rc15).
175+
"""
176+
pattern = release_tag_re(version)
177+
ga: List[str] = []
178+
candidates: List[Tuple[int, str]] = []
179+
for name in tag_names:
180+
match = pattern.match(name)
181+
if match is None:
182+
continue
183+
number = match.group(1)
184+
if number is None:
185+
ga.append(name)
186+
else:
187+
candidates.append((int(number), name))
188+
selected = sorted(ga)
189+
if candidates:
190+
selected.append(max(candidates)[1])
191+
return [f"refs/tags/{name}" for name in selected]
192+
193+
194+
def get_release_tags(client: GitHubClient, version: str) -> List[str]:
195+
# matching-refs returns every ref under the prefix in a single request;
196+
# listing /tags would page through pytorch/pytorch's entire tag history. The
197+
# prefix is not anchored (a ``v2.1`` prefix also matches ``v2.1.0``), so the
198+
# names are still filtered against the exact tag pattern.
199+
refs = client.request(
200+
"GET", f"/repos/{TARGET_REPO}/git/matching-refs/tags/v{version}"
201+
).json()
202+
names = [str(ref["ref"]).removeprefix("refs/tags/") for ref in refs]
203+
return select_target_tags(names, version)
204+
205+
148206
def get_target_refs(client: GitHubClient) -> List[str]:
207+
version = get_candidate_version()
149208
anchor = get_test_version_anchor()
150209
log(f"Test-channel version anchor: release/{anchor[0]}.{anchor[1]}")
151210
branches = client.paginate(
152211
f"/repos/{TARGET_REPO}/branches", params={"protected": "true"}
153212
)
154-
return select_target_refs((branch["name"] for branch in branches), anchor)
213+
refs = select_target_refs((branch["name"] for branch in branches), anchor)
214+
tags = get_release_tags(client, version)
215+
if not tags:
216+
# Expected between the candidate bump and the first RC tag.
217+
log(f"No release tags cut yet for v{version}")
218+
return refs + tags
155219

156220

157221
# --- Desired state: workflow discovery -------------------------------------
@@ -301,7 +365,7 @@ def discover_release_workflows(
301365
"""
302366
paths_by_ref: Dict[str, Set[str]] = {}
303367
for ref in refs:
304-
rev = ref.removeprefix("refs/heads/")
368+
rev = ref.removeprefix("refs/heads/").removeprefix("refs/tags/")
305369
paths = collect_release_workflow_paths(fetch_workflow_files(client, rev))
306370
log(f"Discovered {len(paths)} release workflow(s) on {TARGET_REPO}@{rev}:")
307371
for path in sorted(paths):

tools/tests/test_release_manage_runner_groups.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,44 @@ def test_sorts_release_numerically_not_lexically(self) -> None:
8181
)
8282

8383

84+
class TestSelectTargetTags(TestCase):
85+
def test_ga_and_newest_candidate(self) -> None:
86+
names = ["v2.14.0-rc1", "v2.14.0-rc2", "v2.14.0"]
87+
self.assertEqual(
88+
m.select_target_tags(names, "2.14.0"),
89+
["refs/tags/v2.14.0", "refs/tags/v2.14.0-rc2"],
90+
)
91+
92+
def test_candidate_only_before_ga(self) -> None:
93+
self.assertEqual(
94+
m.select_target_tags(["v2.14.0-rc1"], "2.14.0"),
95+
["refs/tags/v2.14.0-rc1"],
96+
)
97+
98+
def test_sorts_candidates_numerically_not_lexically(self) -> None:
99+
# rc9 must rank below rc10 despite string ordering.
100+
names = ["v2.13.0-rc9", "v2.13.0-rc10"]
101+
self.assertEqual(
102+
m.select_target_tags(names, "2.13.0"),
103+
["refs/tags/v2.13.0-rc10"],
104+
)
105+
106+
def test_ignores_other_versions(self) -> None:
107+
# matching-refs is a prefix query, so v2.1 also returns v2.1x tags.
108+
names = ["v2.1.0", "v2.1.0-rc1", "v2.14.0-rc1"]
109+
self.assertEqual(
110+
m.select_target_tags(names, "2.1.0"),
111+
["refs/tags/v2.1.0", "refs/tags/v2.1.0-rc1"],
112+
)
113+
114+
def test_ignores_unconventional_suffixes(self) -> None:
115+
names = ["v2.14.0-rc1-test", "v2.14.0rc1", "2.14.0-rc1", "v2.14.0-rc"]
116+
self.assertEqual(m.select_target_tags(names, "2.14.0"), [])
117+
118+
def test_no_tags_yet(self) -> None:
119+
self.assertEqual(m.select_target_tags([], "2.14.0"), [])
120+
121+
84122
class TestUsesReleaseLabel(TestCase):
85123
def test_matches_release_labels(self) -> None:
86124
self.assertTrue(m.uses_release_label("runs-on: rel-l-x86iavx512-44-340"))

0 commit comments

Comments
 (0)