|
7 | 7 |
|
8 | 8 | - ensure ``pytorch/pytorch`` is an allowed repository (add-only), and |
9 | 9 | - 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 |
11 | 11 | around the test-channel version (the ``release/X.Y`` anchor read from |
12 | 12 | ``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). |
14 | 16 |
|
15 | 17 | Release workflows are discovered, not hardcoded, by the release runner label |
16 | 18 | they run on (the ``rel-`` marker, e.g. ``rel-l-x86iavx512-44-340``). An entry |
@@ -145,13 +147,89 @@ def select_target_refs( |
145 | 147 | return [f"refs/heads/{name}" for name in selected] |
146 | 148 |
|
147 | 149 |
|
| 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 | + |
148 | 217 | def get_target_refs(client: GitHubClient) -> List[str]: |
149 | 218 | anchor = get_test_version_anchor() |
150 | 219 | log(f"Test-channel version anchor: release/{anchor[0]}.{anchor[1]}") |
151 | 220 | branches = client.paginate( |
152 | 221 | f"/repos/{TARGET_REPO}/branches", params={"protected": "true"} |
153 | 222 | ) |
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 |
155 | 233 |
|
156 | 234 |
|
157 | 235 | # --- Desired state: workflow discovery ------------------------------------- |
@@ -301,7 +379,7 @@ def discover_release_workflows( |
301 | 379 | """ |
302 | 380 | paths_by_ref: Dict[str, Set[str]] = {} |
303 | 381 | for ref in refs: |
304 | | - rev = ref.removeprefix("refs/heads/") |
| 382 | + rev = ref.removeprefix("refs/heads/").removeprefix("refs/tags/") |
305 | 383 | paths = collect_release_workflow_paths(fetch_workflow_files(client, rev)) |
306 | 384 | log(f"Discovered {len(paths)} release workflow(s) on {TARGET_REPO}@{rev}:") |
307 | 385 | for path in sorted(paths): |
|
0 commit comments