-
Notifications
You must be signed in to change notification settings - Fork 245
ci: require tag-triggered artifacts for release uploads #1606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rwgk
wants to merge
1
commit into
NVIDIA:main
Choose a base branch
from
rwgk:semantic-release
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Validate downloaded release wheels against the requested release tag.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import re | ||
| import sys | ||
| from collections import defaultdict | ||
| from pathlib import Path | ||
|
|
||
| COMPONENT_TO_DISTRIBUTIONS: dict[str, set[str]] = { | ||
| "cuda-core": {"cuda_core"}, | ||
| "cuda-bindings": {"cuda_bindings"}, | ||
| "cuda-pathfinder": {"cuda_pathfinder"}, | ||
| "cuda-python": {"cuda_python"}, | ||
| "all": {"cuda_core", "cuda_bindings", "cuda_pathfinder", "cuda_python"}, | ||
| } | ||
|
|
||
| TAG_PATTERNS = ( | ||
| re.compile(r"^v(?P<version>\d+\.\d+\.\d+)"), | ||
| re.compile(r"^cuda-core-v(?P<version>\d+\.\d+\.\d+)"), | ||
| re.compile(r"^cuda-pathfinder-v(?P<version>\d+\.\d+\.\d+)"), | ||
| ) | ||
|
|
||
|
|
||
| def parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser( | ||
| description=( | ||
| "Validate that wheel versions match the release tag. " | ||
| "This rejects dev/local wheel versions for release uploads." | ||
| ) | ||
| ) | ||
| parser.add_argument("git_tag", help="Release git tag (for example: v13.0.0)") | ||
| parser.add_argument("component", choices=sorted(COMPONENT_TO_DISTRIBUTIONS.keys())) | ||
| parser.add_argument("wheel_dir", help="Directory containing wheel files") | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def version_from_tag(tag: str) -> str: | ||
| for pattern in TAG_PATTERNS: | ||
| match = pattern.match(tag) | ||
| if match: | ||
| return match.group("version") | ||
| raise ValueError( | ||
| "Unsupported git tag format " | ||
| f"{tag!r}; expected tags beginning with vX.Y.Z, cuda-core-vX.Y.Z, " | ||
| "or cuda-pathfinder-vX.Y.Z." | ||
| ) | ||
|
|
||
|
|
||
| def parse_wheel_dist_and_version(path: Path) -> tuple[str, str]: | ||
| # Wheel name format starts with: {distribution}-{version}-... | ||
| parts = path.stem.split("-") | ||
| if len(parts) < 5: | ||
| raise ValueError(f"Invalid wheel filename format: {path.name}") | ||
| return parts[0], parts[1] | ||
|
|
||
|
|
||
| def main() -> int: | ||
| args = parse_args() | ||
| expected_version = version_from_tag(args.git_tag) | ||
| expected_distributions = COMPONENT_TO_DISTRIBUTIONS[args.component] | ||
| wheel_dir = Path(args.wheel_dir) | ||
|
|
||
| wheels = sorted(wheel_dir.glob("*.whl")) | ||
| if not wheels: | ||
| print(f"Error: No wheel files found in {wheel_dir}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| seen_versions: dict[str, set[str]] = defaultdict(set) | ||
| errors: list[str] = [] | ||
|
|
||
| for wheel in wheels: | ||
| try: | ||
| distribution, version = parse_wheel_dist_and_version(wheel) | ||
| except ValueError as exc: | ||
| errors.append(str(exc)) | ||
| continue | ||
|
|
||
| if distribution not in expected_distributions: | ||
| continue | ||
|
|
||
| seen_versions[distribution].add(version) | ||
|
|
||
| if ".dev" in version or "+" in version: | ||
| errors.append( | ||
| f"{wheel.name}: wheel version {version!r} contains dev/local markers " | ||
| "(.dev or +), which is not allowed for release uploads." | ||
| ) | ||
|
|
||
| if version != expected_version: | ||
| errors.append( | ||
| f"{wheel.name}: wheel version {version!r} does not match expected " | ||
| f"release version {expected_version!r} from git tag {args.git_tag!r}." | ||
| ) | ||
|
|
||
| missing_distributions = sorted(expected_distributions - set(seen_versions)) | ||
| if missing_distributions: | ||
| errors.append("Missing expected component wheels in download set: " + ", ".join(missing_distributions)) | ||
|
|
||
| for distribution, versions in sorted(seen_versions.items()): | ||
| if len(versions) > 1: | ||
| errors.append( | ||
| f"Expected one release version for {distribution}, found multiple: " + ", ".join(sorted(versions)) | ||
| ) | ||
|
|
||
| if errors: | ||
| print("Wheel validation failed:", file=sys.stderr) | ||
| for error in errors: | ||
| print(f" - {error}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| print( | ||
| "Validated release wheels for component " | ||
| f"{args.component} at version {expected_version} from tag {args.git_tag}." | ||
| ) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on offline discussion we have 3 solutions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally: the simplest solution that meets all requirements is the best one. 1 (cloning/duplication) and 2 (convoluting) don't sound like that's the direction.
Regrading 3, sounds like training wheels (no pun intended)? Do we need them, for tagging? This PR seems to be very close to 3 already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be right. I probably should rest and resume tomorrow...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This solution seems fine, except it means we only get CI on tagged commits on main. The "tags" metadata here acts as a filter, so this means "on branch main, only run this when there is a tag matching the pattern". I think we want to do /both/ every commit to main (which is useful for development and also people do like to have "development snapshots" to download) and the tagged commits again. That's why I suggested in (1) that we need to /clone/ the existing CI workflow to trigger on tags so that we also get tagged releases being built. And when I say "clone" it doesn't have to be literally copy-and-paste -- GHA has various ways to reuse code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I stand corrected. I just experimented on my own fork and it does look like pushing a tag causes the same workflow to run on the same commit. The cancelation policy we have in place gets in the way, but we can remove that. So this does seem like a good approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can keep the current cancellation policy, based on this Cursor-generated explanation (it gave me something similar yesterday, which it then distilled into the Auto-cancellation behavior section in the PR description):
on.push.branchesandon.push.tagsare additive (OR), so we still run CI on every push tomain, and we also run CI on matching tag pushes.Concurrency is currently:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}with
cancel-in-progress: true.Since
github.refdiffers (refs/heads/mainvsrefs/tags/<tag>), cancellations are scoped per ref:mainpush cancels oldermainrunmainrunmainrun does not cancel tag runSo we keep the benefit of pruning stale branch CI without hurting tag-triggered release builds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Experimentally, that doesn't seem to be how it works. On my testing on my own fork, the tag-triggered run canceled the branch-triggered run. But I'm fine with merging this and experimenting and changing the cancelation config later if necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Canceled runs can be manually re-triggered. I am comfortable with merging this PR.