Skip to content

Commit 9c5f93d

Browse files
authored
Allow-list the release lines' RC and GA tags for runner groups (#8502)
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 don't authorize those runs — which is why https://github.com/pytorch/pytorch/actions/runs/31528881582 needed the tag added to the runner groups by hand. Emit tag refs alongside the branch refs, for **every pinned release line**, so a patch release on the preceding line keeps runner access too. Each line contributes its newest GA tag and its newest `v<version>-rc<n>`, ranked by `(patch, rc)` so `v2.13.1-rc1` supersedes the rc15 that shipped as `v2.13.0`. Only the newest of each is kept — pinning every tag would grow the allow-list and the per-ref discovery cost by a workflow set per tag (2.13 reached rc15). Eviction is safe: the allow-list is checked when a job starts, so in-flight builds are unaffected. Tags come from `git/matching-refs`, one request instead of paging pytorch/pytorch's whole tag history. ## Testing 8 new unit tests, 22 pass. `lintrunner` clean. End-to-end against the live API in discovery-only mode: ``` Target refs: ['refs/heads/main', 'refs/heads/nightly', 'refs/heads/release/2.14', 'refs/heads/release/2.13', 'refs/tags/v2.14.0-rc1', 'refs/tags/v2.13.0', 'refs/tags/v2.13.0-rc15'] Desired allow-list (39 references): ...generated-linux-binary-manywheel-nightly.yml@refs/tags/v2.14.0-rc1 ``` https://github.com/pytorch/test-infra/actions/runs/31543618002/job/93951275801#step:4:15
1 parent 3987ba1 commit 9c5f93d

2 files changed

Lines changed: 139 additions & 4 deletions

File tree

tools/scripts/release_manage_runner_groups.py

Lines changed: 82 additions & 4 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 each pinned line's release tags (its newest ``v<version>`` and
14+
``v<version>-rc<n>``, which is where the release binaries are actually built
15+
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
@@ -145,13 +147,89 @@ def select_target_refs(
145147
return [f"refs/heads/{name}" for name in selected]
146148

147149

150+
def release_lines(refs: Iterable[str]) -> List[Tuple[int, int]]:
151+
"""The ``(major, minor)`` release lines among the selected branch refs."""
152+
lines = []
153+
for ref in refs:
154+
match = RELEASE_BRANCH_RE.match(ref.removeprefix("refs/heads/"))
155+
if match is not None:
156+
lines.append((int(match.group(1)), int(match.group(2))))
157+
return lines
158+
159+
160+
def release_tag_re(line: Tuple[int, int]) -> "re.Pattern[str]":
161+
"""Matches the GA and release-candidate tags on release line ``X.Y``.
162+
163+
pytorch/pytorch tags releases as ``v<version>``, with candidates numbered
164+
``v<version>-rc<n>``: ``v2.13.0``, ``v2.13.0-rc15``, ``v2.13.1-rc1``.
165+
"""
166+
return re.compile(rf"^v{line[0]}\.{line[1]}\.(\d+)(?:-rc(\d+))?$")
167+
168+
169+
def select_target_tags(tag_names: Iterable[str], line: Tuple[int, int]) -> List[str]:
170+
"""The newest GA tag and the newest release-candidate tag on line ``X.Y``.
171+
172+
Release binaries are built from tags, not branches: pushing ``v2.14.0-rc1``
173+
runs the build workflows at ``refs/tags/v2.14.0-rc1``. GitHub matches
174+
``selected_workflows`` entries on the exact ref, so the
175+
``@refs/heads/release/2.14`` entry does not authorize that run and the tag
176+
has to be pinned in its own right.
177+
178+
Newest wins by ``(patch, rc)``, so a patch release supersedes the line's
179+
previous tags (``v2.13.1-rc1`` over ``v2.13.0-rc15``). Only one of each is
180+
kept: a new tag supersedes the last, and pinning every one would grow both
181+
the allow-list and the per-ref discovery cost by a workflow set per tag
182+
(2.13 reached rc15).
183+
"""
184+
pattern = release_tag_re(line)
185+
ga: List[Tuple[int, str]] = []
186+
candidates: List[Tuple[Tuple[int, int], str]] = []
187+
for name in tag_names:
188+
match = pattern.match(name)
189+
if match is None:
190+
continue
191+
patch, number = int(match.group(1)), match.group(2)
192+
if number is None:
193+
ga.append((patch, name))
194+
else:
195+
candidates.append(((patch, int(number)), name))
196+
selected: List[str] = []
197+
if ga:
198+
selected.append(max(ga)[1])
199+
if candidates:
200+
selected.append(max(candidates)[1])
201+
return [f"refs/tags/{name}" for name in selected]
202+
203+
204+
def get_release_tags(client: GitHubClient, line: Tuple[int, int]) -> List[str]:
205+
# matching-refs returns every ref under the prefix in a single request;
206+
# listing /tags would page through pytorch/pytorch's entire tag history. The
207+
# trailing dot keeps a v2.1. prefix off v2.14.0, and the names are still
208+
# filtered against the exact tag pattern.
209+
prefix = f"v{line[0]}.{line[1]}."
210+
refs = client.request(
211+
"GET", f"/repos/{TARGET_REPO}/git/matching-refs/tags/{prefix}"
212+
).json()
213+
names = [str(ref["ref"]).removeprefix("refs/tags/") for ref in refs]
214+
return select_target_tags(names, line)
215+
216+
148217
def get_target_refs(client: GitHubClient) -> List[str]:
149218
anchor = get_test_version_anchor()
150219
log(f"Test-channel version anchor: release/{anchor[0]}.{anchor[1]}")
151220
branches = client.paginate(
152221
f"/repos/{TARGET_REPO}/branches", params={"protected": "true"}
153222
)
154-
return select_target_refs((branch["name"] for branch in branches), anchor)
223+
refs = select_target_refs((branch["name"] for branch in branches), anchor)
224+
# Every pinned release line gets its tags, not just the candidate's, so a
225+
# patch release on the preceding line keeps runner access too.
226+
tags = [
227+
tag for line in release_lines(refs) for tag in get_release_tags(client, line)
228+
]
229+
if not tags:
230+
# Expected between a branch cut and the line's first RC tag.
231+
log("No release tags cut yet on the pinned release lines")
232+
return refs + tags
155233

156234

157235
# --- Desired state: workflow discovery -------------------------------------
@@ -301,7 +379,7 @@ def discover_release_workflows(
301379
"""
302380
paths_by_ref: Dict[str, Set[str]] = {}
303381
for ref in refs:
304-
rev = ref.removeprefix("refs/heads/")
382+
rev = ref.removeprefix("refs/heads/").removeprefix("refs/tags/")
305383
paths = collect_release_workflow_paths(fetch_workflow_files(client, rev))
306384
log(f"Discovered {len(paths)} release workflow(s) on {TARGET_REPO}@{rev}:")
307385
for path in sorted(paths):

tools/tests/test_release_manage_runner_groups.py

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

8383

84+
class TestReleaseLines(TestCase):
85+
def test_only_release_branches(self) -> None:
86+
refs = [
87+
"refs/heads/main",
88+
"refs/heads/nightly",
89+
"refs/heads/release/2.14",
90+
"refs/heads/release/2.13",
91+
]
92+
self.assertEqual(m.release_lines(refs), [(2, 14), (2, 13)])
93+
94+
95+
class TestSelectTargetTags(TestCase):
96+
def test_ga_and_newest_candidate(self) -> None:
97+
names = ["v2.14.0-rc1", "v2.14.0-rc2", "v2.14.0"]
98+
self.assertEqual(
99+
m.select_target_tags(names, (2, 14)),
100+
["refs/tags/v2.14.0", "refs/tags/v2.14.0-rc2"],
101+
)
102+
103+
def test_candidate_only_before_ga(self) -> None:
104+
self.assertEqual(
105+
m.select_target_tags(["v2.14.0-rc1"], (2, 14)),
106+
["refs/tags/v2.14.0-rc1"],
107+
)
108+
109+
def test_patch_release_supersedes_shipped_tags(self) -> None:
110+
# The preceding line is still patchable: v2.13.1-rc1 must win over the
111+
# rc15 that shipped as v2.13.0.
112+
names = ["v2.13.0-rc15", "v2.13.0", "v2.13.1-rc1"]
113+
self.assertEqual(
114+
m.select_target_tags(names, (2, 13)),
115+
["refs/tags/v2.13.0", "refs/tags/v2.13.1-rc1"],
116+
)
117+
118+
def test_sorts_candidates_numerically_not_lexically(self) -> None:
119+
# rc9 must rank below rc10 despite string ordering.
120+
names = ["v2.13.0-rc9", "v2.13.0-rc10"]
121+
self.assertEqual(
122+
m.select_target_tags(names, (2, 13)),
123+
["refs/tags/v2.13.0-rc10"],
124+
)
125+
126+
def test_ignores_other_release_lines(self) -> None:
127+
names = ["v2.1.0", "v2.1.0-rc1", "v2.14.0-rc1", "v3.1.0-rc1"]
128+
self.assertEqual(
129+
m.select_target_tags(names, (2, 1)),
130+
["refs/tags/v2.1.0", "refs/tags/v2.1.0-rc1"],
131+
)
132+
133+
def test_ignores_unconventional_tags(self) -> None:
134+
names = ["v2.14.0-rc1-test", "v2.14.0rc1", "2.14.0-rc1", "v2.14.0-rc", "v2.14"]
135+
self.assertEqual(m.select_target_tags(names, (2, 14)), [])
136+
137+
def test_no_tags_yet(self) -> None:
138+
self.assertEqual(m.select_target_tags([], (2, 14)), [])
139+
140+
84141
class TestUsesReleaseLabel(TestCase):
85142
def test_matches_release_labels(self) -> None:
86143
self.assertTrue(m.uses_release_label("runs-on: rel-l-x86iavx512-44-340"))

0 commit comments

Comments
 (0)