From e64deb49d5ce127926149a55e2f8538c8df74d01 Mon Sep 17 00:00:00 2001 From: isVoid <13521008+isVoid@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:58:47 +0000 Subject: [PATCH 1/5] ci: automate WoA cross-repository dispatch --- .github/workflows/woa-main-automation.yml | 257 +++++++++++++++++++ ci/tools/woa_xrepo_select.py | 292 ++++++++++++++++++++++ 2 files changed, 549 insertions(+) create mode 100644 .github/workflows/woa-main-automation.yml create mode 100644 ci/tools/woa_xrepo_select.py diff --git a/.github/workflows/woa-main-automation.yml b/.github/workflows/woa-main-automation.yml new file mode 100644 index 00000000000..c5b1c0287c6 --- /dev/null +++ b/.github/workflows/woa-main-automation.yml @@ -0,0 +1,257 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: "WoA public-main automation" + +on: + workflow_run: + workflows: + - CI + types: + - completed + schedule: + - cron: "17 * * * *" + workflow_dispatch: + inputs: + public_run_id: + description: Optional exact public CI run; empty selects the newest eligible build + required: false + type: string + +permissions: {} + +jobs: + select: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion != 'cancelled' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: read + checks: read + contents: read + concurrency: + group: cuda-python-woa-public-selector + cancel-in-progress: false + outputs: + artifacts-json: ${{ steps.select.outputs.artifacts_json }} + baseline-sha: ${{ steps.select.outputs.baseline_sha }} + commit-count: ${{ steps.select.outputs.commit_count }} + correlation-id: ${{ steps.select.outputs.correlation_id }} + dispatch: ${{ steps.select.outputs.dispatch }} + public-run-attempt: ${{ steps.select.outputs.public_run_attempt }} + public-run-id: ${{ steps.select.outputs.public_run_id }} + public-sha: ${{ steps.select.outputs.public_sha }} + steps: + - name: Checkout trusted selector + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + ref: main + + - name: Select exact public batch + id: select + env: + EVENT_CANDIDATE_RUN_ID: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || '' }} + GITHUB_TOKEN: ${{ github.token }} + MANUAL_CANDIDATE_RUN_ID: ${{ inputs.public_run_id }} + run: | + set -euo pipefail + candidate_run_id="$EVENT_CANDIDATE_RUN_ID" + if [[ -n "$MANUAL_CANDIDATE_RUN_ID" ]]; then + candidate_run_id="$MANUAL_CANDIDATE_RUN_ID" + fi + selection=$(python ci/tools/woa_xrepo_select.py --run-id "$candidate_run_id") + jq . <<< "$selection" + echo "dispatch=$(jq -r .dispatch <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "artifacts_json=$(jq -c '.artifacts // []' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "baseline_sha=$(jq -r '.baseline_sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "commit_count=$(jq -r '.commit_count // 0' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "correlation_id=$(jq -r '.correlation_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "public_run_attempt=$(jq -r '.run_attempt // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "public_run_id=$(jq -r '.run_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "public_sha=$(jq -r '.sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "$(jq -r .reason <<< "$selection")" >> "$GITHUB_STEP_SUMMARY" + + dispatch: + needs: select + if: ${{ needs.select.outputs.dispatch == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: {} + steps: + - name: Create private dispatch token + id: private-app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} + private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} + owner: NVIDIA-dev + repositories: cuda-python-private + permission-actions: write + + - name: Dispatch exact private validation + env: + ARTIFACTS_JSON: ${{ needs.select.outputs.artifacts-json }} + BASELINE_SHA: ${{ needs.select.outputs.baseline-sha }} + COMMIT_COUNT: ${{ needs.select.outputs.commit-count }} + CORRELATION_ID: ${{ needs.select.outputs.correlation-id }} + GH_TOKEN: ${{ steps.private-app-token.outputs.token }} + PUBLIC_RUN_ATTEMPT: ${{ needs.select.outputs.public-run-attempt }} + PUBLIC_RUN_ID: ${{ needs.select.outputs.public-run-id }} + PUBLIC_SHA: ${{ needs.select.outputs.public-sha }} + run: | + set -euo pipefail + payload=$(jq -n \ + --arg artifacts_json "$ARTIFACTS_JSON" \ + --arg baseline_sha "$BASELINE_SHA" \ + --arg commit_count "$COMMIT_COUNT" \ + --arg correlation_id "$CORRELATION_ID" \ + --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ + --arg run_id "$PUBLIC_RUN_ID" \ + --arg sha "$PUBLIC_SHA" \ + '{ + ref: "ctk-next", + return_run_details: true, + inputs: { + schema_version: "1", + public_repository: "NVIDIA/cuda-python", + public_repository_id: "381173759", + public_workflow_id: "155304118", + public_run_id: $run_id, + public_run_attempt: $run_attempt, + public_sha: $sha, + artifacts_json: $artifacts_json, + baseline_sha: $baseline_sha, + commit_count: $commit_count, + correlation_id: $correlation_id + } + }') + response=$(gh api \ + --method POST \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ + --input - <<< "$payload") + private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") + private_run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") + jq -e \ + --arg title "WoA validation $CORRELATION_ID" ' + .repository.id == 809898190 and + .path == ".github/workflows/woa-main-validation.yml" and + .event == "workflow_dispatch" and + .head_branch == "ctk-next" and + .display_title == $title + ' <<< "$private_run" >/dev/null + echo "Exact private validation accepted the selected public batch." >> "$GITHUB_STEP_SUMMARY" + + validate-cancellation: + if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'cancelled' }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read + outputs: + correlation-id: ${{ steps.validate.outputs.correlation_id }} + public-run-attempt: ${{ steps.validate.outputs.public_run_attempt }} + public-run-id: ${{ steps.validate.outputs.public_run_id }} + public-sha: ${{ steps.validate.outputs.public_sha }} + steps: + - name: Validate exact cancelled public run + id: validate + env: + GH_TOKEN: ${{ github.token }} + PUBLIC_RUN_ID: ${{ github.event.workflow_run.id }} + run: | + set -euo pipefail + run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID") + jq -e ' + .repository.id == 381173759 and + .workflow_id == 155304118 and + .path == ".github/workflows/ci.yml" and + .event == "push" and + .head_branch == "main" and + .status == "completed" and + .conclusion == "cancelled" + ' <<< "$run" >/dev/null + public_sha=$(jq -r .head_sha <<< "$run") + public_run_attempt=$(jq -r '.run_attempt | tostring' <<< "$run") + correlation_id="v1:381173759:$PUBLIC_RUN_ID:$public_run_attempt:$public_sha" + echo "public_run_id=$PUBLIC_RUN_ID" >> "$GITHUB_OUTPUT" + echo "public_run_attempt=$public_run_attempt" >> "$GITHUB_OUTPUT" + echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT" + echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" + + dispatch-cancellation: + needs: validate-cancellation + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: {} + steps: + - name: Create private dispatch token + id: private-app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} + private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} + owner: NVIDIA-dev + repositories: cuda-python-private + permission-actions: write + + - name: Locate exact private validation and dispatch cancellation + env: + CORRELATION_ID: ${{ needs.validate-cancellation.outputs.correlation-id }} + GH_TOKEN: ${{ steps.private-app-token.outputs.token }} + PUBLIC_RUN_ATTEMPT: ${{ needs.validate-cancellation.outputs.public-run-attempt }} + PUBLIC_RUN_ID: ${{ needs.validate-cancellation.outputs.public-run-id }} + PUBLIC_SHA: ${{ needs.validate-cancellation.outputs.public-sha }} + run: | + set -euo pipefail + runs=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + 'repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/runs?event=workflow_dispatch&per_page=100') + matches=$(jq -c \ + --arg title "WoA validation $CORRELATION_ID" ' + [.workflow_runs[] | select(.display_title == $title)] + ' <<< "$runs") + match_count=$(jq length <<< "$matches") + if (( match_count == 0 )); then + echo "No correlated private validation exists; cancellation is a no-op." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + if (( match_count != 1 )); then + echo "Expected one correlated private validation, found $match_count." >&2 + exit 1 + fi + private_run_id=$(jq -r '.[0].id | tostring' <<< "$matches") + + payload=$(jq -n \ + --arg correlation_id "$CORRELATION_ID" \ + --arg private_run_id "$private_run_id" \ + --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ + --arg run_id "$PUBLIC_RUN_ID" \ + --arg sha "$PUBLIC_SHA" ' + { + ref: "ctk-next", + return_run_details: true, + inputs: { + schema_version: "1", + public_repository_id: "381173759", + public_run_id: $run_id, + public_run_attempt: $run_attempt, + public_sha: $sha, + private_run_id: $private_run_id, + correlation_id: $correlation_id + } + }') + response=$(gh api \ + --method POST \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-cancel.yml/dispatches \ + --input - <<< "$payload") + jq -e '.workflow_run_id | type == "number"' <<< "$response" >/dev/null + echo "Private cancellation controller accepted the exact request." >> "$GITHUB_STEP_SUMMARY" diff --git a/ci/tools/woa_xrepo_select.py b/ci/tools/woa_xrepo_select.py new file mode 100644 index 00000000000..6489fe47104 --- /dev/null +++ b/ci/tools/woa_xrepo_select.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import datetime +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request + + +PUBLIC_REPOSITORY = "NVIDIA/cuda-python" +PUBLIC_REPOSITORY_ID = 381173759 +PUBLIC_WORKFLOW_ID = 155304118 +PUBLIC_WORKFLOW_PATH = ".github/workflows/ci.yml" +REPORTING_APP_ID = 4954254 +CHECK_NAME = "cuda-python WoA integration" +MAX_LEDGER_COMMITS = 100 + + +class GitHubAPI: + def __init__(self, token): + self._token = token + + def get(self, path, params=None): + url = f"https://api.github.com/{path.lstrip('/')}" + if params: + url = f"{url}?{urllib.parse.urlencode(params)}" + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self._token}", + "X-GitHub-Api-Version": "2026-03-10", + }, + ) + try: + with urllib.request.urlopen(request) as response: + return json.load(response) + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + raise RuntimeError(f"GitHub API GET {path} failed: {error.code}: {body}") from error + + def paginate(self, path, item_key, params=None, max_pages=10): + items = [] + query = dict(params or {}) + query["per_page"] = 100 + for page in range(1, max_pages + 1): + query["page"] = page + response = self.get(path, query) + page_items = response[item_key] + items.extend(page_items) + if len(page_items) < 100: + break + return items + + +def parse_time(value): + return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def select_artifacts(artifacts, sha): + binding_name = f"cuda-bindings-python313-cuda13.4.2-win-arm64-{sha}" + core_name = f"cuda-core-python313-win-arm64-{sha}" + selected = [ + artifact + for artifact in artifacts + if not artifact["expired"] + and ( + artifact["name"] == "cuda-pathfinder-wheel" + or artifact["name"] == binding_name + or artifact["name"] == core_name + ) + ] + if len(selected) != 3: + return None + names = {artifact["name"] for artifact in selected} + if len(names) != 3: + return None + result = [] + for artifact in sorted(selected, key=lambda item: item["name"]): + digest = artifact.get("digest") + if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): + return None + result.append( + {"id": artifact["id"], "name": artifact["name"], "digest": digest} + ) + return result + + +def validate_run(api, run): + if not ( + run["repository"]["id"] == PUBLIC_REPOSITORY_ID + and run["workflow_id"] == PUBLIC_WORKFLOW_ID + and run["path"] == PUBLIC_WORKFLOW_PATH + and run["event"] == "push" + and run["head_branch"] == "main" + and run["status"] == "completed" + and run["conclusion"] == "success" + and re.fullmatch(r"[0-9a-f]{40}", run["head_sha"]) + ): + return None + + run_id = run["id"] + attempt = run["run_attempt"] + jobs = api.paginate( + f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/attempts/{attempt}/jobs", + "jobs", + ) + woa_jobs = [job for job in jobs if job["name"].startswith("Build win-arm64, CUDA ")] + if not woa_jobs or any( + job["status"] != "completed" or job["conclusion"] != "success" for job in woa_jobs + ): + return None + + artifacts = api.paginate( + f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", "artifacts" + ) + selected_artifacts = select_artifacts(artifacts, run["head_sha"]) + if selected_artifacts is None: + return None + + comparison = api.get( + f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main" + ) + if comparison["status"] not in {"ahead", "identical"}: + return None + + return { + "run_id": str(run_id), + "run_attempt": str(attempt), + "sha": run["head_sha"], + "artifacts": selected_artifacts, + } + + +def resolve_candidate(api, run_id): + if run_id: + run = api.get(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}") + return validate_run(api, run) + + runs = api.get( + f"repos/{PUBLIC_REPOSITORY}/actions/workflows/ci.yml/runs", + { + "branch": "main", + "event": "push", + "status": "completed", + "per_page": 30, + }, + )["workflow_runs"] + for run in runs: + candidate = validate_run(api, run) + if candidate is not None: + return candidate + return None + + +def app_checks(api, sha): + response = api.get( + f"repos/{PUBLIC_REPOSITORY}/commits/{sha}/check-runs", + {"check_name": CHECK_NAME, "per_page": 100}, + ) + return [ + check + for check in response["check_runs"] + if check["name"] == CHECK_NAME and check["app"]["id"] == REPORTING_APP_ID + ] + + +def list_commits(api): + return api.get( + f"repos/{PUBLIC_REPOSITORY}/commits", + {"sha": "main", "per_page": MAX_LEDGER_COMMITS}, + ) + + +def select_batch(api, candidate, batch_size, max_wait_seconds, now): + candidate_checks = app_checks(api, candidate["sha"]) + external_id = ( + f"woa:v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:" + f"{candidate['run_attempt']}:{candidate['sha']}" + ) + if any( + check.get("external_id") == external_id + for check in candidate_checks + ): + return {"dispatch": False, "reason": "candidate already has an App-owned attempt"} + + commits = list_commits(api) + try: + candidate_index = next( + index for index, commit in enumerate(commits) if commit["sha"] == candidate["sha"] + ) + except StopIteration as error: + raise RuntimeError( + f"candidate SHA was not found on the latest {MAX_LEDGER_COMMITS} main commits" + ) from error + + baseline_index = None + baseline_sha = "" + for index in range(candidate_index + 1, len(commits)): + checks = app_checks(api, commits[index]["sha"]) + successful = [ + check + for check in checks + if check["status"] == "completed" and check["conclusion"] == "success" + ] + if successful: + baseline_index = index + baseline_sha = commits[index]["sha"] + break + + if baseline_index is None: + return { + "dispatch": True, + "reason": ( + "bootstrap: no successful App-owned baseline exists in the bounded ledger" + ), + "baseline_sha": "", + "commit_count": 1, + } + + commit_count = baseline_index - candidate_index + oldest_pending = commits[baseline_index - 1] + oldest_time = parse_time(oldest_pending["commit"]["committer"]["date"]) + age_seconds = max(0, int((now - oldest_time).total_seconds())) + if commit_count >= batch_size: + reason = f"batch threshold reached: {commit_count} commits" + dispatch = True + elif age_seconds >= max_wait_seconds: + reason = f"maximum wait reached: oldest pending commit is {age_seconds} seconds old" + dispatch = True + else: + reason = ( + f"deferred: {commit_count}/{batch_size} commits and oldest pending age " + f"{age_seconds}/{max_wait_seconds} seconds" + ) + dispatch = False + return { + "dispatch": dispatch, + "reason": reason, + "baseline_sha": baseline_sha, + "commit_count": commit_count, + } + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--run-id", default="") + parser.add_argument("--batch-size", type=int, default=3) + parser.add_argument("--max-wait-seconds", type=int, default=7200) + args = parser.parse_args() + if args.run_id and not args.run_id.isdigit(): + parser.error("--run-id must be numeric") + if args.batch_size < 1 or args.max_wait_seconds < 1: + parser.error("batch size and maximum wait must be positive") + + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise SystemExit("GITHUB_TOKEN is required") + api = GitHubAPI(token) + candidate = resolve_candidate(api, args.run_id) + if candidate is None: + print(json.dumps({"dispatch": False, "reason": "no eligible public WoA build"})) + return + + selection = select_batch( + api, + candidate, + args.batch_size, + args.max_wait_seconds, + datetime.datetime.now(datetime.timezone.utc), + ) + selection.update(candidate) + selection["correlation_id"] = ( + f"v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:" + f"{candidate['run_attempt']}:{candidate['sha']}" + ) + print(json.dumps(selection, separators=(",", ":"), sort_keys=True)) + + +if __name__ == "__main__": + try: + main() + except Exception as error: + print(f"error: {error}", file=sys.stderr) + raise From bf3af4ed248d1f287e52bf3a83b26e96bef2a7dc Mon Sep 17 00:00:00 2001 From: isVoid <13521008+isVoid@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:03:28 +0000 Subject: [PATCH 2/5] ci: satisfy selector lint policy --- ci/tools/woa_xrepo_select.py | 88 ++++++++++++++---------------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/ci/tools/woa_xrepo_select.py b/ci/tools/woa_xrepo_select.py index 6489fe47104..4fd9740cf9b 100644 --- a/ci/tools/woa_xrepo_select.py +++ b/ci/tools/woa_xrepo_select.py @@ -5,14 +5,12 @@ import argparse import datetime +import http.client import json import os import re import sys -import urllib.error import urllib.parse -import urllib.request - PUBLIC_REPOSITORY = "NVIDIA/cuda-python" PUBLIC_REPOSITORY_ID = 381173759 @@ -28,23 +26,28 @@ def __init__(self, token): self._token = token def get(self, path, params=None): - url = f"https://api.github.com/{path.lstrip('/')}" + request_path = f"/{path.lstrip('/')}" if params: - url = f"{url}?{urllib.parse.urlencode(params)}" - request = urllib.request.Request( - url, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {self._token}", - "X-GitHub-Api-Version": "2026-03-10", - }, - ) + request_path = f"{request_path}?{urllib.parse.urlencode(params)}" + connection = http.client.HTTPSConnection("api.github.com") try: - with urllib.request.urlopen(request) as response: - return json.load(response) - except urllib.error.HTTPError as error: - body = error.read().decode("utf-8", errors="replace") - raise RuntimeError(f"GitHub API GET {path} failed: {error.code}: {body}") from error + connection.request( + "GET", + request_path, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self._token}", + "X-GitHub-Api-Version": "2026-03-10", + "User-Agent": "cuda-python-woa-selector", + }, + ) + response = connection.getresponse() + body = response.read().decode("utf-8", errors="replace") + if response.status >= 400: + raise RuntimeError(f"GitHub API GET {path} failed: {response.status}: {body}") + return json.loads(body) + finally: + connection.close() def paginate(self, path, item_key, params=None, max_pages=10): items = [] @@ -73,7 +76,7 @@ def select_artifacts(artifacts, sha): if not artifact["expired"] and ( artifact["name"] == "cuda-pathfinder-wheel" - or artifact["name"] == binding_name + or artifact["name"] == binding_name or artifact["name"] == core_name ) ] @@ -87,9 +90,7 @@ def select_artifacts(artifacts, sha): digest = artifact.get("digest") if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): return None - result.append( - {"id": artifact["id"], "name": artifact["name"], "digest": digest} - ) + result.append({"id": artifact["id"], "name": artifact["name"], "digest": digest}) return result @@ -113,21 +114,15 @@ def validate_run(api, run): "jobs", ) woa_jobs = [job for job in jobs if job["name"].startswith("Build win-arm64, CUDA ")] - if not woa_jobs or any( - job["status"] != "completed" or job["conclusion"] != "success" for job in woa_jobs - ): + if not woa_jobs or any(job["status"] != "completed" or job["conclusion"] != "success" for job in woa_jobs): return None - artifacts = api.paginate( - f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", "artifacts" - ) + artifacts = api.paginate(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", "artifacts") selected_artifacts = select_artifacts(artifacts, run["head_sha"]) if selected_artifacts is None: return None - comparison = api.get( - f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main" - ) + comparison = api.get(f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main") if comparison["status"] not in {"ahead", "identical"}: return None @@ -181,35 +176,21 @@ def list_commits(api): def select_batch(api, candidate, batch_size, max_wait_seconds, now): candidate_checks = app_checks(api, candidate["sha"]) - external_id = ( - f"woa:v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:" - f"{candidate['run_attempt']}:{candidate['sha']}" - ) - if any( - check.get("external_id") == external_id - for check in candidate_checks - ): + external_id = f"woa:v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:{candidate['run_attempt']}:{candidate['sha']}" + if any(check.get("external_id") == external_id for check in candidate_checks): return {"dispatch": False, "reason": "candidate already has an App-owned attempt"} commits = list_commits(api) try: - candidate_index = next( - index for index, commit in enumerate(commits) if commit["sha"] == candidate["sha"] - ) + candidate_index = next(index for index, commit in enumerate(commits) if commit["sha"] == candidate["sha"]) except StopIteration as error: - raise RuntimeError( - f"candidate SHA was not found on the latest {MAX_LEDGER_COMMITS} main commits" - ) from error + raise RuntimeError(f"candidate SHA was not found on the latest {MAX_LEDGER_COMMITS} main commits") from error baseline_index = None baseline_sha = "" for index in range(candidate_index + 1, len(commits)): checks = app_checks(api, commits[index]["sha"]) - successful = [ - check - for check in checks - if check["status"] == "completed" and check["conclusion"] == "success" - ] + successful = [check for check in checks if check["status"] == "completed" and check["conclusion"] == "success"] if successful: baseline_index = index baseline_sha = commits[index]["sha"] @@ -218,9 +199,7 @@ def select_batch(api, candidate, batch_size, max_wait_seconds, now): if baseline_index is None: return { "dispatch": True, - "reason": ( - "bootstrap: no successful App-owned baseline exists in the bounded ledger" - ), + "reason": ("bootstrap: no successful App-owned baseline exists in the bounded ledger"), "baseline_sha": "", "commit_count": 1, } @@ -278,8 +257,7 @@ def main(): ) selection.update(candidate) selection["correlation_id"] = ( - f"v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:" - f"{candidate['run_attempt']}:{candidate['sha']}" + f"v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:{candidate['run_attempt']}:{candidate['sha']}" ) print(json.dumps(selection, separators=(",", ":"), sort_keys=True)) From 9cd019a72c4f18230f28a46680296d9927ee5ae7 Mon Sep 17 00:00:00 2001 From: isVoid <13521008+isVoid@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:27:47 +0000 Subject: [PATCH 3/5] ci: dispatch exact WoA producer job --- .github/workflows/woa-main-automation.yml | 5 ++ ci/tools/woa_xrepo_select.py | 56 +++++++++++++---------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/.github/workflows/woa-main-automation.yml b/.github/workflows/woa-main-automation.yml index c5b1c0287c6..4ce1ecd46cb 100644 --- a/.github/workflows/woa-main-automation.yml +++ b/.github/workflows/woa-main-automation.yml @@ -39,6 +39,7 @@ jobs: commit-count: ${{ steps.select.outputs.commit_count }} correlation-id: ${{ steps.select.outputs.correlation_id }} dispatch: ${{ steps.select.outputs.dispatch }} + public-producer-job-id: ${{ steps.select.outputs.public_producer_job_id }} public-run-attempt: ${{ steps.select.outputs.public_run_attempt }} public-run-id: ${{ steps.select.outputs.public_run_id }} public-sha: ${{ steps.select.outputs.public_sha }} @@ -69,6 +70,7 @@ jobs: echo "baseline_sha=$(jq -r '.baseline_sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "commit_count=$(jq -r '.commit_count // 0' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "correlation_id=$(jq -r '.correlation_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" + echo "public_producer_job_id=$(jq -r '.producer_job_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "public_run_attempt=$(jq -r '.run_attempt // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "public_run_id=$(jq -r '.run_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "public_sha=$(jq -r '.sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" @@ -98,6 +100,7 @@ jobs: COMMIT_COUNT: ${{ needs.select.outputs.commit-count }} CORRELATION_ID: ${{ needs.select.outputs.correlation-id }} GH_TOKEN: ${{ steps.private-app-token.outputs.token }} + PUBLIC_PRODUCER_JOB_ID: ${{ needs.select.outputs.public-producer-job-id }} PUBLIC_RUN_ATTEMPT: ${{ needs.select.outputs.public-run-attempt }} PUBLIC_RUN_ID: ${{ needs.select.outputs.public-run-id }} PUBLIC_SHA: ${{ needs.select.outputs.public-sha }} @@ -108,6 +111,7 @@ jobs: --arg baseline_sha "$BASELINE_SHA" \ --arg commit_count "$COMMIT_COUNT" \ --arg correlation_id "$CORRELATION_ID" \ + --arg producer_job_id "$PUBLIC_PRODUCER_JOB_ID" \ --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ --arg run_id "$PUBLIC_RUN_ID" \ --arg sha "$PUBLIC_SHA" \ @@ -119,6 +123,7 @@ jobs: public_repository: "NVIDIA/cuda-python", public_repository_id: "381173759", public_workflow_id: "155304118", + public_producer_job_id: $producer_job_id, public_run_id: $run_id, public_run_attempt: $run_attempt, public_sha: $sha, diff --git a/ci/tools/woa_xrepo_select.py b/ci/tools/woa_xrepo_select.py index 4fd9740cf9b..7263d852584 100644 --- a/ci/tools/woa_xrepo_select.py +++ b/ci/tools/woa_xrepo_select.py @@ -18,6 +18,7 @@ PUBLIC_WORKFLOW_PATH = ".github/workflows/ci.yml" REPORTING_APP_ID = 4954254 CHECK_NAME = "cuda-python WoA integration" +PRODUCER_JOB_NAME = "Build win-arm64, CUDA 13.4.2 / py3.13" MAX_LEDGER_COMMITS = 100 @@ -67,31 +68,34 @@ def parse_time(value): return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) -def select_artifacts(artifacts, sha): - binding_name = f"cuda-bindings-python313-cuda13.4.2-win-arm64-{sha}" - core_name = f"cuda-core-python313-win-arm64-{sha}" - selected = [ - artifact - for artifact in artifacts - if not artifact["expired"] - and ( - artifact["name"] == "cuda-pathfinder-wheel" - or artifact["name"] == binding_name - or artifact["name"] == core_name - ) +def select_artifacts(api, run_id, sha): + names = [ + "cuda-pathfinder-wheel", + f"cuda-bindings-python313-cuda13.4.2-win-arm64-{sha}", + f"cuda-core-python313-win-arm64-{sha}", ] - if len(selected) != 3: - return None - names = {artifact["name"] for artifact in selected} - if len(names) != 3: - return None result = [] - for artifact in sorted(selected, key=lambda item: item["name"]): + for name in names: + response = api.get( + f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", + {"name": name, "per_page": 100}, + ) + artifacts = response["artifacts"] + if response["total_count"] != 1 or len(artifacts) != 1: + return None + artifact = artifacts[0] digest = artifact.get("digest") - if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): + if not ( + artifact["name"] == name + and artifact["expired"] is False + and isinstance(artifact["id"], int) + and artifact["id"] > 0 + and isinstance(digest, str) + and re.fullmatch(r"sha256:[0-9a-f]{64}", digest) + ): return None result.append({"id": artifact["id"], "name": artifact["name"], "digest": digest}) - return result + return sorted(result, key=lambda item: item["name"]) def validate_run(api, run): @@ -113,12 +117,15 @@ def validate_run(api, run): f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/attempts/{attempt}/jobs", "jobs", ) - woa_jobs = [job for job in jobs if job["name"].startswith("Build win-arm64, CUDA ")] - if not woa_jobs or any(job["status"] != "completed" or job["conclusion"] != "success" for job in woa_jobs): + producer_jobs = [job for job in jobs if job["name"] == PRODUCER_JOB_NAME] + if not ( + len(producer_jobs) == 1 + and producer_jobs[0]["status"] == "completed" + and producer_jobs[0]["conclusion"] == "success" + ): return None - artifacts = api.paginate(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", "artifacts") - selected_artifacts = select_artifacts(artifacts, run["head_sha"]) + selected_artifacts = select_artifacts(api, run_id, run["head_sha"]) if selected_artifacts is None: return None @@ -129,6 +136,7 @@ def validate_run(api, run): return { "run_id": str(run_id), "run_attempt": str(attempt), + "producer_job_id": str(producer_jobs[0]["id"]), "sha": run["head_sha"], "artifacts": selected_artifacts, } From 94be0cfc54897181a33f40fae653bb172af71a5f Mon Sep 17 00:00:00 2001 From: isVoid <13521008+isVoid@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:16:07 +0000 Subject: [PATCH 4/5] ci: start WoA validation after artifact builds --- .github/workflows/ci.yml | 19 ++ .github/workflows/woa-crossrepo-smoke.yml | 106 --------- .github/workflows/woa-main-automation.yml | 132 ++++-------- .../woa-private-dispatch-reusable.yml | 149 +++++++++++++ .github/workflows/woa-private-dispatch.yml | 179 +--------------- ci/tools/woa_xrepo_resolve.py | 202 ++++++++++++++++++ ci/tools/woa_xrepo_select.py | 162 +------------- 7 files changed, 424 insertions(+), 525 deletions(-) delete mode 100644 .github/workflows/woa-crossrepo-smoke.yml create mode 100644 .github/workflows/woa-private-dispatch-reusable.yml create mode 100644 ci/tools/woa_xrepo_resolve.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 313be5ff77e..18635230ffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,6 +466,25 @@ jobs: workplan: ${{ needs.detect-changes.outputs.workplan }} single-cuda-major: ${{ needs.ci-vars.outputs.WINDOWS_ARM64_SINGLE_CUDA_MAJOR == 'true' }} + woa-main-dispatch: + name: Dispatch WoA integration + needs: + - build-linux-64 + - build-windows-arm64 + if: ${{ github.repository_owner == 'nvidia' && + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.build-linux-64.result == 'success' && + needs.build-windows-arm64.result == 'success' }} + permissions: + actions: read + checks: read + contents: read + uses: ./.github/workflows/woa-main-automation.yml + with: + public_run_id: ${{ github.run_id }} + secrets: inherit + # NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*) # so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by # build_hooks.py) are exercised on their target OS. Keep these job definitions diff --git a/.github/workflows/woa-crossrepo-smoke.yml b/.github/workflows/woa-crossrepo-smoke.yml deleted file mode 100644 index 4f2fd49f913..00000000000 --- a/.github/workflows/woa-crossrepo-smoke.yml +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -name: "WoA cross-repository transport smoke" -run-name: "WoA transport smoke for PR #${{ inputs.pr_number }}" - -on: - workflow_dispatch: - inputs: - pr_number: - description: Open same-repository PR to receive the smoke Check Run - required: true - type: number - -permissions: {} - -jobs: - resolve-public-pr: - if: ${{ github.repository == 'NVIDIA/cuda-python' }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - pull-requests: read - outputs: - correlation-id: ${{ steps.resolve.outputs.correlation_id }} - public-sha: ${{ steps.resolve.outputs.public_sha }} - steps: - - name: Resolve exact same-repository PR head - id: resolve - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ inputs.pr_number }} - run: | - set -euo pipefail - [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] - pr=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/pulls/$PR_NUMBER") - jq -e ' - .state == "open" and - .base.ref == "main" and - .head.repo.full_name == "NVIDIA/cuda-python" and - (.head.sha | test("^[0-9a-f]{40}$")) - ' <<< "$pr" >/dev/null - public_sha=$(jq -r .head.sha <<< "$pr") - correlation_id="smoke:381173759:$PR_NUMBER:$public_sha:${GITHUB_RUN_ID}:${GITHUB_RUN_ATTEMPT}" - echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT" - echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" - - dispatch-private-smoke: - needs: resolve-public-pr - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: {} - steps: - - name: Create private dispatch token - id: private-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} - owner: NVIDIA-dev - repositories: cuda-python-private - permission-actions: write - - - name: Dispatch and verify private smoke workflow - env: - CORRELATION_ID: ${{ needs.resolve-public-pr.outputs.correlation-id }} - GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PR_NUMBER: ${{ inputs.pr_number }} - PUBLIC_SHA: ${{ needs.resolve-public-pr.outputs.public-sha }} - run: | - set -euo pipefail - payload=$(jq -n \ - --arg correlation_id "$CORRELATION_ID" \ - --arg pr_number "$PR_NUMBER" \ - --arg public_sha "$PUBLIC_SHA" \ - '{ - ref: "ctk-next", - return_run_details: true, - inputs: { - schema_version: "1", - public_repository: "NVIDIA/cuda-python", - public_repository_id: "381173759", - public_pr_number: $pr_number, - public_sha: $public_sha, - correlation_id: $correlation_id - } - }') - response=$(gh api \ - --method POST \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-crossrepo-smoke.yml/dispatches \ - --input - <<< "$payload") - private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") - run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") - jq -e ' - .repository.id == 809898190 and - .path == ".github/workflows/woa-crossrepo-smoke.yml" and - .event == "workflow_dispatch" and - .head_branch == "ctk-next" - ' <<< "$run" >/dev/null - echo "Private transport-smoke workflow accepted the exact dispatch." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/woa-main-automation.yml b/.github/workflows/woa-main-automation.yml index 4ce1ecd46cb..d1736650b40 100644 --- a/.github/workflows/woa-main-automation.yml +++ b/.github/workflows/woa-main-automation.yml @@ -5,6 +5,12 @@ name: "WoA public-main automation" on: + workflow_call: + inputs: + public_run_id: + description: Exact in-progress public CI run with completed WoA producers + required: true + type: string workflow_run: workflows: - CI @@ -23,7 +29,7 @@ permissions: {} jobs: select: - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion != 'cancelled' }} + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -34,15 +40,10 @@ jobs: group: cuda-python-woa-public-selector cancel-in-progress: false outputs: - artifacts-json: ${{ steps.select.outputs.artifacts_json }} baseline-sha: ${{ steps.select.outputs.baseline_sha }} commit-count: ${{ steps.select.outputs.commit_count }} - correlation-id: ${{ steps.select.outputs.correlation_id }} dispatch: ${{ steps.select.outputs.dispatch }} - public-producer-job-id: ${{ steps.select.outputs.public_producer_job_id }} - public-run-attempt: ${{ steps.select.outputs.public_run_attempt }} public-run-id: ${{ steps.select.outputs.public_run_id }} - public-sha: ${{ steps.select.outputs.public_sha }} steps: - name: Checkout trusted selector uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -56,104 +57,36 @@ jobs: env: EVENT_CANDIDATE_RUN_ID: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || '' }} GITHUB_TOKEN: ${{ github.token }} - MANUAL_CANDIDATE_RUN_ID: ${{ inputs.public_run_id }} + INPUT_CANDIDATE_RUN_ID: ${{ inputs.public_run_id }} run: | set -euo pipefail candidate_run_id="$EVENT_CANDIDATE_RUN_ID" - if [[ -n "$MANUAL_CANDIDATE_RUN_ID" ]]; then - candidate_run_id="$MANUAL_CANDIDATE_RUN_ID" + if [[ -n "$INPUT_CANDIDATE_RUN_ID" ]]; then + candidate_run_id="$INPUT_CANDIDATE_RUN_ID" fi selection=$(python ci/tools/woa_xrepo_select.py --run-id "$candidate_run_id") jq . <<< "$selection" echo "dispatch=$(jq -r .dispatch <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "artifacts_json=$(jq -c '.artifacts // []' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "baseline_sha=$(jq -r '.baseline_sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "commit_count=$(jq -r '.commit_count // 0' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "correlation_id=$(jq -r '.correlation_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "public_producer_job_id=$(jq -r '.producer_job_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "public_run_attempt=$(jq -r '.run_attempt // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "public_run_id=$(jq -r '.run_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "public_sha=$(jq -r '.sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" echo "$(jq -r .reason <<< "$selection")" >> "$GITHUB_STEP_SUMMARY" dispatch: needs: select if: ${{ needs.select.outputs.dispatch == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: {} - steps: - - name: Create private dispatch token - id: private-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} - owner: NVIDIA-dev - repositories: cuda-python-private - permission-actions: write - - - name: Dispatch exact private validation - env: - ARTIFACTS_JSON: ${{ needs.select.outputs.artifacts-json }} - BASELINE_SHA: ${{ needs.select.outputs.baseline-sha }} - COMMIT_COUNT: ${{ needs.select.outputs.commit-count }} - CORRELATION_ID: ${{ needs.select.outputs.correlation-id }} - GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PUBLIC_PRODUCER_JOB_ID: ${{ needs.select.outputs.public-producer-job-id }} - PUBLIC_RUN_ATTEMPT: ${{ needs.select.outputs.public-run-attempt }} - PUBLIC_RUN_ID: ${{ needs.select.outputs.public-run-id }} - PUBLIC_SHA: ${{ needs.select.outputs.public-sha }} - run: | - set -euo pipefail - payload=$(jq -n \ - --arg artifacts_json "$ARTIFACTS_JSON" \ - --arg baseline_sha "$BASELINE_SHA" \ - --arg commit_count "$COMMIT_COUNT" \ - --arg correlation_id "$CORRELATION_ID" \ - --arg producer_job_id "$PUBLIC_PRODUCER_JOB_ID" \ - --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ - --arg run_id "$PUBLIC_RUN_ID" \ - --arg sha "$PUBLIC_SHA" \ - '{ - ref: "ctk-next", - return_run_details: true, - inputs: { - schema_version: "1", - public_repository: "NVIDIA/cuda-python", - public_repository_id: "381173759", - public_workflow_id: "155304118", - public_producer_job_id: $producer_job_id, - public_run_id: $run_id, - public_run_attempt: $run_attempt, - public_sha: $sha, - artifacts_json: $artifacts_json, - baseline_sha: $baseline_sha, - commit_count: $commit_count, - correlation_id: $correlation_id - } - }') - response=$(gh api \ - --method POST \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ - --input - <<< "$payload") - private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") - private_run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") - jq -e \ - --arg title "WoA validation $CORRELATION_ID" ' - .repository.id == 809898190 and - .path == ".github/workflows/woa-main-validation.yml" and - .event == "workflow_dispatch" and - .head_branch == "ctk-next" and - .display_title == $title - ' <<< "$private_run" >/dev/null - echo "Exact private validation accepted the selected public batch." >> "$GITHUB_STEP_SUMMARY" + permissions: + actions: read + contents: read + uses: ./.github/workflows/woa-private-dispatch-reusable.yml + with: + public_run_id: ${{ needs.select.outputs.public-run-id }} + baseline_sha: ${{ needs.select.outputs.baseline-sha }} + commit_count: ${{ needs.select.outputs.commit-count }} + secrets: inherit - validate-cancellation: - if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'cancelled' }} + validate-terminal-failure: + if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion != 'success' }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -164,7 +97,7 @@ jobs: public-run-id: ${{ steps.validate.outputs.public_run_id }} public-sha: ${{ steps.validate.outputs.public_sha }} steps: - - name: Validate exact cancelled public run + - name: Validate exact unsuccessful public run id: validate env: GH_TOKEN: ${{ github.token }} @@ -181,7 +114,14 @@ jobs: .event == "push" and .head_branch == "main" and .status == "completed" and - .conclusion == "cancelled" + (.conclusion | IN( + "action_required", + "cancelled", + "failure", + "stale", + "startup_failure", + "timed_out" + )) ' <<< "$run" >/dev/null public_sha=$(jq -r .head_sha <<< "$run") public_run_attempt=$(jq -r '.run_attempt | tostring' <<< "$run") @@ -192,7 +132,7 @@ jobs: echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" dispatch-cancellation: - needs: validate-cancellation + needs: validate-terminal-failure runs-on: ubuntu-latest timeout-minutes: 10 permissions: {} @@ -209,11 +149,11 @@ jobs: - name: Locate exact private validation and dispatch cancellation env: - CORRELATION_ID: ${{ needs.validate-cancellation.outputs.correlation-id }} + CORRELATION_ID: ${{ needs.validate-terminal-failure.outputs.correlation-id }} GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PUBLIC_RUN_ATTEMPT: ${{ needs.validate-cancellation.outputs.public-run-attempt }} - PUBLIC_RUN_ID: ${{ needs.validate-cancellation.outputs.public-run-id }} - PUBLIC_SHA: ${{ needs.validate-cancellation.outputs.public-sha }} + PUBLIC_RUN_ATTEMPT: ${{ needs.validate-terminal-failure.outputs.public-run-attempt }} + PUBLIC_RUN_ID: ${{ needs.validate-terminal-failure.outputs.public-run-id }} + PUBLIC_SHA: ${{ needs.validate-terminal-failure.outputs.public-sha }} run: | set -euo pipefail runs=$(gh api \ @@ -225,7 +165,7 @@ jobs: ' <<< "$runs") match_count=$(jq length <<< "$matches") if (( match_count == 0 )); then - echo "No correlated private validation exists; cancellation is a no-op." >> "$GITHUB_STEP_SUMMARY" + echo "No correlated private validation exists; terminal reconciliation is a no-op." >> "$GITHUB_STEP_SUMMARY" exit 0 fi if (( match_count != 1 )); then diff --git a/.github/workflows/woa-private-dispatch-reusable.yml b/.github/workflows/woa-private-dispatch-reusable.yml new file mode 100644 index 00000000000..9b54b3f9511 --- /dev/null +++ b/.github/workflows/woa-private-dispatch-reusable.yml @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: "WoA reusable private dispatch" +run-name: "WoA broker for public run ${{ inputs.public_run_id }}" + +on: + workflow_call: + inputs: + public_run_id: + description: Exact eligible public CI run ID + required: true + type: string + baseline_sha: + description: Last successfully validated public main SHA, if any + required: false + default: "" + type: string + commit_count: + description: Number of public main commits represented by this validation + required: false + default: "1" + type: string + secrets: + CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY: + required: true + +permissions: {} + +jobs: + resolve-public-build: + if: ${{ github.repository == 'NVIDIA/cuda-python' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + outputs: + artifacts-json: ${{ steps.resolve.outputs.artifacts_json }} + correlation-id: ${{ steps.resolve.outputs.correlation_id }} + public-producer-jobs-json: ${{ steps.resolve.outputs.public_producer_jobs_json }} + public-run-attempt: ${{ steps.resolve.outputs.public_run_attempt }} + public-sha: ${{ steps.resolve.outputs.public_sha }} + steps: + - name: Checkout trusted resolver + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Resolve exact successful public producers + id: resolve + env: + GITHUB_TOKEN: ${{ github.token }} + PUBLIC_RUN_ID: ${{ inputs.public_run_id }} + run: | + set -euo pipefail + resolution=$(python ci/tools/woa_xrepo_resolve.py --run-id "$PUBLIC_RUN_ID") + jq . <<< "$resolution" + echo "artifacts_json=$(jq -c .artifacts <<< "$resolution")" >> "$GITHUB_OUTPUT" + echo "correlation_id=$(jq -r .correlation_id <<< "$resolution")" >> "$GITHUB_OUTPUT" + echo "public_producer_jobs_json=$(jq -c .producer_jobs <<< "$resolution")" >> "$GITHUB_OUTPUT" + echo "public_run_attempt=$(jq -r .run_attempt <<< "$resolution")" >> "$GITHUB_OUTPUT" + echo "public_sha=$(jq -r .sha <<< "$resolution")" >> "$GITHUB_OUTPUT" + { + echo "Resolved public run \`$PUBLIC_RUN_ID\` attempt \`$(jq -r .run_attempt <<< "$resolution")\`." + echo + echo "Verified producers:" + jq -r '.producer_jobs[] | "- `\(.name)` (job `\(.id)`)"' <<< "$resolution" + echo + echo "Exact Windows Arm64 artifacts:" + jq -r '.artifacts[] | "- `\(.name)` (`\(.digest)`)"' <<< "$resolution" + } >> "$GITHUB_STEP_SUMMARY" + + dispatch-private-validation: + needs: resolve-public-build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: {} + steps: + - name: Create private dispatch token + id: private-app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} + private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} + owner: NVIDIA-dev + repositories: cuda-python-private + permission-actions: write + + - name: Dispatch and verify exact private validation + env: + ARTIFACTS_JSON: ${{ needs.resolve-public-build.outputs.artifacts-json }} + BASELINE_SHA: ${{ inputs.baseline_sha }} + COMMIT_COUNT: ${{ inputs.commit_count }} + CORRELATION_ID: ${{ needs.resolve-public-build.outputs.correlation-id }} + GH_TOKEN: ${{ steps.private-app-token.outputs.token }} + PUBLIC_PRODUCER_JOBS_JSON: ${{ needs.resolve-public-build.outputs.public-producer-jobs-json }} + PUBLIC_RUN_ATTEMPT: ${{ needs.resolve-public-build.outputs.public-run-attempt }} + PUBLIC_RUN_ID: ${{ inputs.public_run_id }} + PUBLIC_SHA: ${{ needs.resolve-public-build.outputs.public-sha }} + run: | + set -euo pipefail + payload=$(jq -n \ + --arg artifacts_json "$ARTIFACTS_JSON" \ + --arg baseline_sha "$BASELINE_SHA" \ + --arg commit_count "$COMMIT_COUNT" \ + --arg correlation_id "$CORRELATION_ID" \ + --arg producer_jobs_json "$PUBLIC_PRODUCER_JOBS_JSON" \ + --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ + --arg run_id "$PUBLIC_RUN_ID" \ + --arg sha "$PUBLIC_SHA" ' + { + ref: "ctk-next", + return_run_details: true, + inputs: { + schema_version: "2", + public_repository: "NVIDIA/cuda-python", + public_repository_id: "381173759", + public_workflow_id: "155304118", + public_producer_jobs_json: $producer_jobs_json, + public_run_id: $run_id, + public_run_attempt: $run_attempt, + public_sha: $sha, + artifacts_json: $artifacts_json, + baseline_sha: $baseline_sha, + commit_count: $commit_count, + correlation_id: $correlation_id + } + }') + response=$(gh api \ + --method POST \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ + --input - <<< "$payload") + private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") + private_run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") + jq -e \ + --arg title "WoA validation $CORRELATION_ID" ' + .repository.id == 809898190 and + .path == ".github/workflows/woa-main-validation.yml" and + .event == "workflow_dispatch" and + .head_branch == "ctk-next" and + .display_title == $title + ' <<< "$private_run" >/dev/null + echo "Exact private validation accepted the selected public batch." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/woa-private-dispatch.yml b/.github/workflows/woa-private-dispatch.yml index e01fd107243..b4820f79f54 100644 --- a/.github/workflows/woa-private-dispatch.yml +++ b/.github/workflows/woa-private-dispatch.yml @@ -9,184 +9,21 @@ on: workflow_dispatch: inputs: public_run_id: - description: Successful public main CI run containing the exact WoA artifacts + description: Eligible public main CI run containing the exact WoA artifacts required: true type: string permissions: {} jobs: - resolve-public-build: + dispatch: if: ${{ github.repository == 'NVIDIA/cuda-python' }} - runs-on: ubuntu-latest - timeout-minutes: 10 permissions: actions: read contents: read - checks: read - outputs: - artifacts-json: ${{ steps.resolve.outputs.artifacts_json }} - correlation-id: ${{ steps.resolve.outputs.correlation_id }} - public-producer-job-id: ${{ steps.resolve.outputs.public_producer_job_id }} - public-run-attempt: ${{ steps.resolve.outputs.public_run_attempt }} - public-sha: ${{ steps.resolve.outputs.public_sha }} - steps: - - name: Resolve exact successful public build - id: resolve - env: - GH_TOKEN: ${{ github.token }} - PUBLIC_RUN_ID: ${{ inputs.public_run_id }} - run: | - set -euo pipefail - [[ "$PUBLIC_RUN_ID" =~ ^[1-9][0-9]*$ ]] - run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID") - jq -e ' - .repository.id == 381173759 and - .workflow_id == 155304118 and - .path == ".github/workflows/ci.yml" and - .event == "push" and - .head_branch == "main" and - .status == "completed" and - .conclusion == "success" and - (.head_sha | test("^[0-9a-f]{40}$")) - ' <<< "$run" >/dev/null - public_sha=$(jq -r .head_sha <<< "$run") - public_run_attempt=$(jq -r '.run_attempt | tostring' <<< "$run") - - comparison=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/compare/$public_sha...main") - jq -e '.status == "ahead" or .status == "identical"' <<< "$comparison" >/dev/null - - jobs=$(gh api --paginate \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID/attempts/$public_run_attempt/jobs?per_page=100" \ - --jq '.jobs') - producer_jobs=$(jq -s -c ' - add - | map(select(.name == "Build win-arm64, CUDA 13.4.2 / py3.13")) - ' <<< "$jobs") - jq -e ' - length == 1 and - .[0].status == "completed" and - .[0].conclusion == "success" - ' <<< "$producer_jobs" >/dev/null - public_producer_job_id=$(jq -r '.[0].id | tostring' <<< "$producer_jobs") - - expected_names=$(jq -n \ - --arg sha "$public_sha" '[ - "cuda-pathfinder-wheel", - ("cuda-bindings-python313-cuda13.4.2-win-arm64-" + $sha), - ("cuda-core-python313-win-arm64-" + $sha) - ]') - artifacts_json='[]' - while IFS= read -r artifact_name; do - response=$(gh api --method GET \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID/artifacts" \ - -f name="$artifact_name" \ - -F per_page=100) - artifact=$(jq -c -e \ - --arg name "$artifact_name" ' - if .total_count == 1 and - (.artifacts | length) == 1 and - .artifacts[0].name == $name and - .artifacts[0].expired == false - then .artifacts[0] | {id, name, digest} - else error("expected exactly one unexpired artifact named " + $name) - end - ' <<< "$response") - artifacts_json=$(jq -c \ - --argjson artifact "$artifact" \ - '. + [$artifact] | sort_by(.name)' <<< "$artifacts_json") - done < <(jq -r '.[]' <<< "$expected_names") - jq -e ' - length == 3 and - all(.[ ]; - (.id | type == "number") and - (.digest | test("^sha256:[0-9a-f]{64}$"))) - ' <<< "$artifacts_json" >/dev/null - - correlation_id="v1:381173759:$PUBLIC_RUN_ID:$public_run_attempt:$public_sha" - echo "artifacts_json=$artifacts_json" >> "$GITHUB_OUTPUT" - echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" - echo "public_producer_job_id=$public_producer_job_id" >> "$GITHUB_OUTPUT" - echo "public_run_attempt=$public_run_attempt" >> "$GITHUB_OUTPUT" - echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT" - { - echo "Resolved public run \`$PUBLIC_RUN_ID\` attempt \`$public_run_attempt\` and producer job \`$public_producer_job_id\`." - echo "" - echo "Exact Windows Arm64 artifacts:" - jq -r '.[] | "- `\(.name)` (`\(.digest)`)"' <<< "$artifacts_json" - } >> "$GITHUB_STEP_SUMMARY" - - dispatch-private-validation: - needs: resolve-public-build - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: {} - steps: - - name: Create private dispatch token - id: private-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} - owner: NVIDIA-dev - repositories: cuda-python-private - permission-actions: write - - - name: Dispatch and verify exact private validation - env: - ARTIFACTS_JSON: ${{ needs.resolve-public-build.outputs.artifacts-json }} - CORRELATION_ID: ${{ needs.resolve-public-build.outputs.correlation-id }} - GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PUBLIC_RUN_ATTEMPT: ${{ needs.resolve-public-build.outputs.public-run-attempt }} - PUBLIC_RUN_ID: ${{ inputs.public_run_id }} - PUBLIC_PRODUCER_JOB_ID: ${{ needs.resolve-public-build.outputs.public-producer-job-id }} - PUBLIC_SHA: ${{ needs.resolve-public-build.outputs.public-sha }} - run: | - set -euo pipefail - payload=$(jq -n \ - --arg artifacts_json "$ARTIFACTS_JSON" \ - --arg correlation_id "$CORRELATION_ID" \ - --arg producer_job_id "$PUBLIC_PRODUCER_JOB_ID" \ - --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ - --arg run_id "$PUBLIC_RUN_ID" \ - --arg sha "$PUBLIC_SHA" \ - '{ - ref: "ctk-next", - return_run_details: true, - inputs: { - schema_version: "1", - public_repository: "NVIDIA/cuda-python", - public_repository_id: "381173759", - public_workflow_id: "155304118", - public_producer_job_id: $producer_job_id, - public_run_id: $run_id, - public_run_attempt: $run_attempt, - public_sha: $sha, - artifacts_json: $artifacts_json, - baseline_sha: "", - commit_count: "1", - correlation_id: $correlation_id - } - }') - response=$(gh api \ - --method POST \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ - --input - <<< "$payload") - private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") - run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") - jq -e ' - .repository.id == 809898190 and - .path == ".github/workflows/woa-main-validation.yml" and - .event == "workflow_dispatch" and - .head_branch == "ctk-next" - ' <<< "$run" >/dev/null - echo "Private validation workflow accepted the exact dispatch." >> "$GITHUB_STEP_SUMMARY" + uses: ./.github/workflows/woa-private-dispatch-reusable.yml + with: + public_run_id: ${{ inputs.public_run_id }} + baseline_sha: "" + commit_count: "1" + secrets: inherit diff --git a/ci/tools/woa_xrepo_resolve.py b/ci/tools/woa_xrepo_resolve.py new file mode 100644 index 00000000000..cb299af13dd --- /dev/null +++ b/ci/tools/woa_xrepo_resolve.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import http.client +import json +import os +import re +import sys +import urllib.parse + +PUBLIC_REPOSITORY = "NVIDIA/cuda-python" +PUBLIC_REPOSITORY_ID = 381173759 +PUBLIC_WORKFLOW_ID = 155304118 +PUBLIC_WORKFLOW_PATH = ".github/workflows/ci.yml" +REPORTING_APP_ID = 4954254 +CHECK_NAME = "cuda-python WoA integration" +CUDA_BUILD_VERSION = "13.4.2" +PYTHON_VERSIONS = ("3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t") + + +class GitHubAPI: + def __init__(self, token): + self._token = token + + def get(self, path, params=None): + request_path = f"/{path.lstrip('/')}" + if params: + request_path = f"{request_path}?{urllib.parse.urlencode(params)}" + connection = http.client.HTTPSConnection("api.github.com") + try: + connection.request( + "GET", + request_path, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self._token}", + "X-GitHub-Api-Version": "2026-03-10", + "User-Agent": "cuda-python-woa-resolver", + }, + ) + response = connection.getresponse() + body = response.read().decode("utf-8", errors="replace") + if response.status >= 400: + raise RuntimeError(f"GitHub API GET {path} failed: {response.status}: {body}") + return json.loads(body) + finally: + connection.close() + + def paginate(self, path, item_key, params=None, max_pages=10): + items = [] + query = dict(params or {}) + query["per_page"] = 100 + for page in range(1, max_pages + 1): + query["page"] = page + response = self.get(path, query) + page_items = response[item_key] + items.extend(page_items) + if len(page_items) < 100: + break + return items + + +def expected_producer_names(): + names = [f"Build linux-64, CUDA {CUDA_BUILD_VERSION} / py3.10"] + names.extend(f"Build win-arm64, CUDA {CUDA_BUILD_VERSION} / py{version}" for version in PYTHON_VERSIONS) + return names + + +def expected_artifact_names(sha): + names = ["cuda-pathfinder-wheel"] + for version in PYTHON_VERSIONS: + python_tag = version.replace(".", "") + names.extend( + ( + f"cuda-bindings-python{python_tag}-cuda{CUDA_BUILD_VERSION}-win-arm64-{sha}", + f"cuda-core-python{python_tag}-win-arm64-{sha}", + ) + ) + return names + + +def select_artifacts(api, run_id, sha): + result = [] + for name in expected_artifact_names(sha): + response = api.get( + f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", + {"name": name, "per_page": 100}, + ) + artifacts = response["artifacts"] + if response["total_count"] != 1 or len(artifacts) != 1: + return None + artifact = artifacts[0] + digest = artifact.get("digest") + if not ( + artifact["name"] == name + and artifact["expired"] is False + and isinstance(artifact["id"], int) + and artifact["id"] > 0 + and isinstance(digest, str) + and re.fullmatch(r"sha256:[0-9a-f]{64}", digest) + ): + return None + result.append({"id": artifact["id"], "name": artifact["name"], "digest": digest}) + return sorted(result, key=lambda item: item["name"]) + + +def run_is_admissible(run): + state_is_admissible = (run["status"] == "in_progress" and run["conclusion"] is None) or ( + run["status"] == "completed" and run["conclusion"] == "success" + ) + return ( + run["repository"]["id"] == PUBLIC_REPOSITORY_ID + and run["workflow_id"] == PUBLIC_WORKFLOW_ID + and run["path"] == PUBLIC_WORKFLOW_PATH + and run["event"] == "push" + and run["head_branch"] == "main" + and state_is_admissible + and re.fullmatch(r"[0-9a-f]{40}", run["head_sha"]) + ) + + +def validate_run(api, run): + if not run_is_admissible(run): + return None + + run_id = run["id"] + attempt = run["run_attempt"] + jobs = api.paginate( + f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/attempts/{attempt}/jobs", + "jobs", + ) + producer_jobs = [] + for name in expected_producer_names(): + matches = [job for job in jobs if job["name"] == name] + if not (len(matches) == 1 and matches[0]["status"] == "completed" and matches[0]["conclusion"] == "success"): + return None + producer_jobs.append({"id": matches[0]["id"], "name": name}) + + selected_artifacts = select_artifacts(api, run_id, run["head_sha"]) + if selected_artifacts is None: + return None + + comparison = api.get(f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main") + if comparison["status"] not in {"ahead", "identical"}: + return None + + return { + "run_id": str(run_id), + "run_attempt": str(attempt), + "producer_jobs": sorted(producer_jobs, key=lambda item: item["name"]), + "sha": run["head_sha"], + "artifacts": selected_artifacts, + "correlation_id": f"v1:{PUBLIC_REPOSITORY_ID}:{run_id}:{attempt}:{run['head_sha']}", + } + + +def resolve_candidate(api, run_id): + if run_id: + run = api.get(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}") + return validate_run(api, run) + + runs = api.get( + f"repos/{PUBLIC_REPOSITORY}/actions/workflows/ci.yml/runs", + { + "branch": "main", + "event": "push", + "status": "completed", + "per_page": 30, + }, + )["workflow_runs"] + for run in runs: + candidate = validate_run(api, run) + if candidate is not None: + return candidate + return None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--run-id", required=True) + args = parser.parse_args() + if not args.run_id.isdigit(): + parser.error("--run-id must be numeric") + + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise SystemExit("GITHUB_TOKEN is required") + candidate = resolve_candidate(GitHubAPI(token), args.run_id) + if candidate is None: + raise RuntimeError("the requested public run is not ready for WoA validation") + print(json.dumps(candidate, separators=(",", ":"), sort_keys=True)) + + +if __name__ == "__main__": + try: + main() + except Exception as error: + print(f"error: {error}", file=sys.stderr) + raise diff --git a/ci/tools/woa_xrepo_select.py b/ci/tools/woa_xrepo_select.py index 7263d852584..edab836b1a0 100644 --- a/ci/tools/woa_xrepo_select.py +++ b/ci/tools/woa_xrepo_select.py @@ -5,164 +5,25 @@ import argparse import datetime -import http.client import json import os -import re import sys -import urllib.parse -PUBLIC_REPOSITORY = "NVIDIA/cuda-python" -PUBLIC_REPOSITORY_ID = 381173759 -PUBLIC_WORKFLOW_ID = 155304118 -PUBLIC_WORKFLOW_PATH = ".github/workflows/ci.yml" -REPORTING_APP_ID = 4954254 -CHECK_NAME = "cuda-python WoA integration" -PRODUCER_JOB_NAME = "Build win-arm64, CUDA 13.4.2 / py3.13" -MAX_LEDGER_COMMITS = 100 - - -class GitHubAPI: - def __init__(self, token): - self._token = token +from woa_xrepo_resolve import ( + CHECK_NAME, + PUBLIC_REPOSITORY, + REPORTING_APP_ID, + GitHubAPI, + resolve_candidate, +) - def get(self, path, params=None): - request_path = f"/{path.lstrip('/')}" - if params: - request_path = f"{request_path}?{urllib.parse.urlencode(params)}" - connection = http.client.HTTPSConnection("api.github.com") - try: - connection.request( - "GET", - request_path, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {self._token}", - "X-GitHub-Api-Version": "2026-03-10", - "User-Agent": "cuda-python-woa-selector", - }, - ) - response = connection.getresponse() - body = response.read().decode("utf-8", errors="replace") - if response.status >= 400: - raise RuntimeError(f"GitHub API GET {path} failed: {response.status}: {body}") - return json.loads(body) - finally: - connection.close() - - def paginate(self, path, item_key, params=None, max_pages=10): - items = [] - query = dict(params or {}) - query["per_page"] = 100 - for page in range(1, max_pages + 1): - query["page"] = page - response = self.get(path, query) - page_items = response[item_key] - items.extend(page_items) - if len(page_items) < 100: - break - return items +MAX_LEDGER_COMMITS = 100 def parse_time(value): return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) -def select_artifacts(api, run_id, sha): - names = [ - "cuda-pathfinder-wheel", - f"cuda-bindings-python313-cuda13.4.2-win-arm64-{sha}", - f"cuda-core-python313-win-arm64-{sha}", - ] - result = [] - for name in names: - response = api.get( - f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", - {"name": name, "per_page": 100}, - ) - artifacts = response["artifacts"] - if response["total_count"] != 1 or len(artifacts) != 1: - return None - artifact = artifacts[0] - digest = artifact.get("digest") - if not ( - artifact["name"] == name - and artifact["expired"] is False - and isinstance(artifact["id"], int) - and artifact["id"] > 0 - and isinstance(digest, str) - and re.fullmatch(r"sha256:[0-9a-f]{64}", digest) - ): - return None - result.append({"id": artifact["id"], "name": artifact["name"], "digest": digest}) - return sorted(result, key=lambda item: item["name"]) - - -def validate_run(api, run): - if not ( - run["repository"]["id"] == PUBLIC_REPOSITORY_ID - and run["workflow_id"] == PUBLIC_WORKFLOW_ID - and run["path"] == PUBLIC_WORKFLOW_PATH - and run["event"] == "push" - and run["head_branch"] == "main" - and run["status"] == "completed" - and run["conclusion"] == "success" - and re.fullmatch(r"[0-9a-f]{40}", run["head_sha"]) - ): - return None - - run_id = run["id"] - attempt = run["run_attempt"] - jobs = api.paginate( - f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/attempts/{attempt}/jobs", - "jobs", - ) - producer_jobs = [job for job in jobs if job["name"] == PRODUCER_JOB_NAME] - if not ( - len(producer_jobs) == 1 - and producer_jobs[0]["status"] == "completed" - and producer_jobs[0]["conclusion"] == "success" - ): - return None - - selected_artifacts = select_artifacts(api, run_id, run["head_sha"]) - if selected_artifacts is None: - return None - - comparison = api.get(f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main") - if comparison["status"] not in {"ahead", "identical"}: - return None - - return { - "run_id": str(run_id), - "run_attempt": str(attempt), - "producer_job_id": str(producer_jobs[0]["id"]), - "sha": run["head_sha"], - "artifacts": selected_artifacts, - } - - -def resolve_candidate(api, run_id): - if run_id: - run = api.get(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}") - return validate_run(api, run) - - runs = api.get( - f"repos/{PUBLIC_REPOSITORY}/actions/workflows/ci.yml/runs", - { - "branch": "main", - "event": "push", - "status": "completed", - "per_page": 30, - }, - )["workflow_runs"] - for run in runs: - candidate = validate_run(api, run) - if candidate is not None: - return candidate - return None - - def app_checks(api, sha): response = api.get( f"repos/{PUBLIC_REPOSITORY}/commits/{sha}/check-runs", @@ -184,7 +45,7 @@ def list_commits(api): def select_batch(api, candidate, batch_size, max_wait_seconds, now): candidate_checks = app_checks(api, candidate["sha"]) - external_id = f"woa:v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:{candidate['run_attempt']}:{candidate['sha']}" + external_id = f"woa:{candidate['correlation_id']}" if any(check.get("external_id") == external_id for check in candidate_checks): return {"dispatch": False, "reason": "candidate already has an App-owned attempt"} @@ -207,7 +68,7 @@ def select_batch(api, candidate, batch_size, max_wait_seconds, now): if baseline_index is None: return { "dispatch": True, - "reason": ("bootstrap: no successful App-owned baseline exists in the bounded ledger"), + "reason": "bootstrap: no successful App-owned baseline exists in the bounded ledger", "baseline_sha": "", "commit_count": 1, } @@ -264,9 +125,6 @@ def main(): datetime.datetime.now(datetime.timezone.utc), ) selection.update(candidate) - selection["correlation_id"] = ( - f"v1:{PUBLIC_REPOSITORY_ID}:{candidate['run_id']}:{candidate['run_attempt']}:{candidate['sha']}" - ) print(json.dumps(selection, separators=(",", ":"), sort_keys=True)) From 7c7674bf58de4bf4bbdb95addd4bef2bc4d77fc9 Mon Sep 17 00:00:00 2001 From: isVoid <13521008+isVoid@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:39:07 +0000 Subject: [PATCH 5/5] ci: reuse the WoA manual broker for automation --- .github/workflows/woa-main-automation.yml | 240 +++++++----------- .../woa-private-dispatch-reusable.yml | 149 ----------- .github/workflows/woa-private-dispatch.yml | 199 ++++++++++++++- ci/tools/woa_xrepo_resolve.py | 202 --------------- ci/tools/woa_xrepo_select.py | 136 ---------- 5 files changed, 286 insertions(+), 640 deletions(-) delete mode 100644 .github/workflows/woa-private-dispatch-reusable.yml delete mode 100644 ci/tools/woa_xrepo_resolve.py delete mode 100644 ci/tools/woa_xrepo_select.py diff --git a/.github/workflows/woa-main-automation.yml b/.github/workflows/woa-main-automation.yml index d1736650b40..8a3b2d203ea 100644 --- a/.github/workflows/woa-main-automation.yml +++ b/.github/workflows/woa-main-automation.yml @@ -8,20 +8,17 @@ on: workflow_call: inputs: public_run_id: - description: Exact in-progress public CI run with completed WoA producers required: true type: string - workflow_run: - workflows: - - CI - types: - - completed + secrets: + CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY: + required: true schedule: - cron: "17 * * * *" workflow_dispatch: inputs: public_run_id: - description: Optional exact public CI run; empty selects the newest eligible build + description: Optional exact public CI run; empty selects the newest successful build required: false type: string @@ -29,9 +26,9 @@ permissions: {} jobs: select: - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.repository == 'NVIDIA/cuda-python' }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 10 permissions: actions: read checks: read @@ -45,158 +42,109 @@ jobs: dispatch: ${{ steps.select.outputs.dispatch }} public-run-id: ${{ steps.select.outputs.public_run_id }} steps: - - name: Checkout trusted selector - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - persist-credentials: false - ref: main - - - name: Select exact public batch + - name: Select public batch id: select - env: - EVENT_CANDIDATE_RUN_ID: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || '' }} - GITHUB_TOKEN: ${{ github.token }} - INPUT_CANDIDATE_RUN_ID: ${{ inputs.public_run_id }} - run: | - set -euo pipefail - candidate_run_id="$EVENT_CANDIDATE_RUN_ID" - if [[ -n "$INPUT_CANDIDATE_RUN_ID" ]]; then - candidate_run_id="$INPUT_CANDIDATE_RUN_ID" - fi - selection=$(python ci/tools/woa_xrepo_select.py --run-id "$candidate_run_id") - jq . <<< "$selection" - echo "dispatch=$(jq -r .dispatch <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "baseline_sha=$(jq -r '.baseline_sha // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "commit_count=$(jq -r '.commit_count // 0' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "public_run_id=$(jq -r '.run_id // ""' <<< "$selection")" >> "$GITHUB_OUTPUT" - echo "$(jq -r .reason <<< "$selection")" >> "$GITHUB_STEP_SUMMARY" - - dispatch: - needs: select - if: ${{ needs.select.outputs.dispatch == 'true' }} - permissions: - actions: read - contents: read - uses: ./.github/workflows/woa-private-dispatch-reusable.yml - with: - public_run_id: ${{ needs.select.outputs.public-run-id }} - baseline_sha: ${{ needs.select.outputs.baseline-sha }} - commit_count: ${{ needs.select.outputs.commit-count }} - secrets: inherit - - validate-terminal-failure: - if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion != 'success' }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - actions: read - outputs: - correlation-id: ${{ steps.validate.outputs.correlation_id }} - public-run-attempt: ${{ steps.validate.outputs.public_run_attempt }} - public-run-id: ${{ steps.validate.outputs.public_run_id }} - public-sha: ${{ steps.validate.outputs.public_sha }} - steps: - - name: Validate exact unsuccessful public run - id: validate env: GH_TOKEN: ${{ github.token }} - PUBLIC_RUN_ID: ${{ github.event.workflow_run.id }} + INPUT_RUN_ID: ${{ inputs.public_run_id }} run: | set -euo pipefail - run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID") + if [[ -n "$INPUT_RUN_ID" ]]; then + run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/actions/runs/$INPUT_RUN_ID") + else + run=$(gh api --method GET \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + repos/NVIDIA/cuda-python/actions/workflows/ci.yml/runs \ + -f branch=main -f event=push -f status=success -F per_page=1 \ + --jq '.workflow_runs[0]') + fi jq -e ' .repository.id == 381173759 and .workflow_id == 155304118 and .path == ".github/workflows/ci.yml" and .event == "push" and .head_branch == "main" and - .status == "completed" and - (.conclusion | IN( - "action_required", - "cancelled", - "failure", - "stale", - "startup_failure", - "timed_out" - )) + ((.status == "in_progress" and .conclusion == null) or + (.status == "completed" and .conclusion == "success")) ' <<< "$run" >/dev/null - public_sha=$(jq -r .head_sha <<< "$run") - public_run_attempt=$(jq -r '.run_attempt | tostring' <<< "$run") - correlation_id="v1:381173759:$PUBLIC_RUN_ID:$public_run_attempt:$public_sha" - echo "public_run_id=$PUBLIC_RUN_ID" >> "$GITHUB_OUTPUT" - echo "public_run_attempt=$public_run_attempt" >> "$GITHUB_OUTPUT" - echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT" - echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" - - dispatch-cancellation: - needs: validate-terminal-failure - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: {} - steps: - - name: Create private dispatch token - id: private-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} - owner: NVIDIA-dev - repositories: cuda-python-private - permission-actions: write + run_id=$(jq -r '.id | tostring' <<< "$run") + attempt=$(jq -r '.run_attempt | tostring' <<< "$run") + sha=$(jq -r .head_sha <<< "$run") + external_id="woa:v1:381173759:$run_id:$attempt:$sha" - - name: Locate exact private validation and dispatch cancellation - env: - CORRELATION_ID: ${{ needs.validate-terminal-failure.outputs.correlation-id }} - GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PUBLIC_RUN_ATTEMPT: ${{ needs.validate-terminal-failure.outputs.public-run-attempt }} - PUBLIC_RUN_ID: ${{ needs.validate-terminal-failure.outputs.public-run-id }} - PUBLIC_SHA: ${{ needs.validate-terminal-failure.outputs.public-sha }} - run: | - set -euo pipefail - runs=$(gh api \ + checks=$(gh api \ -H 'X-GitHub-Api-Version: 2026-03-10' \ - 'repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/runs?event=workflow_dispatch&per_page=100') - matches=$(jq -c \ - --arg title "WoA validation $CORRELATION_ID" ' - [.workflow_runs[] | select(.display_title == $title)] - ' <<< "$runs") - match_count=$(jq length <<< "$matches") - if (( match_count == 0 )); then - echo "No correlated private validation exists; terminal reconciliation is a no-op." >> "$GITHUB_STEP_SUMMARY" + "repos/NVIDIA/cuda-python/commits/$sha/check-runs?check_name=cuda-python%20WoA%20integration&per_page=100") + if jq -e --arg id "$external_id" ' + any(.check_runs[]; .app.id == 4954254 and .external_id == $id) + ' <<< "$checks" >/dev/null; then + echo "dispatch=false" >> "$GITHUB_OUTPUT" + echo "This exact public run already has an App-owned attempt." >> "$GITHUB_STEP_SUMMARY" exit 0 fi - if (( match_count != 1 )); then - echo "Expected one correlated private validation, found $match_count." >&2 - exit 1 - fi - private_run_id=$(jq -r '.[0].id | tostring' <<< "$matches") - payload=$(jq -n \ - --arg correlation_id "$CORRELATION_ID" \ - --arg private_run_id "$private_run_id" \ - --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ - --arg run_id "$PUBLIC_RUN_ID" \ - --arg sha "$PUBLIC_SHA" ' - { - ref: "ctk-next", - return_run_details: true, - inputs: { - schema_version: "1", - public_repository_id: "381173759", - public_run_id: $run_id, - public_run_attempt: $run_attempt, - public_sha: $sha, - private_run_id: $private_run_id, - correlation_id: $correlation_id - } - }') - response=$(gh api \ - --method POST \ + commits=$(gh api --method GET \ -H 'X-GitHub-Api-Version: 2026-03-10' \ - repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-cancel.yml/dispatches \ - --input - <<< "$payload") - jq -e '.workflow_run_id | type == "number"' <<< "$response" >/dev/null - echo "Private cancellation controller accepted the exact request." >> "$GITHUB_STEP_SUMMARY" + repos/NVIDIA/cuda-python/commits -f sha=main -F per_page=100) + candidate_index=$(jq -er --arg sha "$sha" ' + [to_entries[] | select(.value.sha == $sha) | .key][0] + ' <<< "$commits") + commit_total=$(jq length <<< "$commits") + baseline_index="" + baseline_sha="" + for ((index = candidate_index + 1; index < commit_total; index++)); do + older_sha=$(jq -r --argjson index "$index" '.[$index].sha' <<< "$commits") + older_checks=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/commits/$older_sha/check-runs?check_name=cuda-python%20WoA%20integration&per_page=100") + if jq -e ' + any(.check_runs[]; + .app.id == 4954254 and + .status == "completed" and + .conclusion == "success") + ' <<< "$older_checks" >/dev/null; then + baseline_index=$index + baseline_sha=$older_sha + break + fi + done + + dispatch=true + commit_count=1 + reason="Bootstrap: no successful App-owned baseline exists in the latest 100 commits." + if [[ -n "$baseline_index" ]]; then + commit_count=$((baseline_index - candidate_index)) + oldest_pending_index=$((baseline_index - 1)) + oldest_pending_at=$(jq -r \ + --argjson index "$oldest_pending_index" \ + '.[$index].commit.committer.date' <<< "$commits") + age_seconds=$(($(date +%s) - $(date -d "$oldest_pending_at" +%s))) + if (( commit_count >= 3 )); then + reason="Batch threshold reached: $commit_count commits." + elif (( age_seconds >= 7200 )); then + reason="Two-hour fallback reached: oldest pending commit is $age_seconds seconds old." + else + dispatch=false + reason="Deferred: $commit_count/3 commits and oldest pending age $age_seconds/7200 seconds." + fi + fi + echo "baseline_sha=$baseline_sha" >> "$GITHUB_OUTPUT" + echo "commit_count=$commit_count" >> "$GITHUB_OUTPUT" + echo "dispatch=$dispatch" >> "$GITHUB_OUTPUT" + echo "public_run_id=$run_id" >> "$GITHUB_OUTPUT" + echo "$reason" >> "$GITHUB_STEP_SUMMARY" + + dispatch: + needs: select + if: ${{ needs.select.outputs.dispatch == 'true' }} + permissions: + actions: read + contents: read + uses: ./.github/workflows/woa-private-dispatch.yml + with: + public_run_id: ${{ needs.select.outputs.public-run-id }} + baseline_sha: ${{ needs.select.outputs.baseline-sha }} + commit_count: ${{ needs.select.outputs.commit-count }} + secrets: inherit diff --git a/.github/workflows/woa-private-dispatch-reusable.yml b/.github/workflows/woa-private-dispatch-reusable.yml deleted file mode 100644 index 9b54b3f9511..00000000000 --- a/.github/workflows/woa-private-dispatch-reusable.yml +++ /dev/null @@ -1,149 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -name: "WoA reusable private dispatch" -run-name: "WoA broker for public run ${{ inputs.public_run_id }}" - -on: - workflow_call: - inputs: - public_run_id: - description: Exact eligible public CI run ID - required: true - type: string - baseline_sha: - description: Last successfully validated public main SHA, if any - required: false - default: "" - type: string - commit_count: - description: Number of public main commits represented by this validation - required: false - default: "1" - type: string - secrets: - CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY: - required: true - -permissions: {} - -jobs: - resolve-public-build: - if: ${{ github.repository == 'NVIDIA/cuda-python' }} - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - actions: read - contents: read - outputs: - artifacts-json: ${{ steps.resolve.outputs.artifacts_json }} - correlation-id: ${{ steps.resolve.outputs.correlation_id }} - public-producer-jobs-json: ${{ steps.resolve.outputs.public_producer_jobs_json }} - public-run-attempt: ${{ steps.resolve.outputs.public_run_attempt }} - public-sha: ${{ steps.resolve.outputs.public_sha }} - steps: - - name: Checkout trusted resolver - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - persist-credentials: false - - - name: Resolve exact successful public producers - id: resolve - env: - GITHUB_TOKEN: ${{ github.token }} - PUBLIC_RUN_ID: ${{ inputs.public_run_id }} - run: | - set -euo pipefail - resolution=$(python ci/tools/woa_xrepo_resolve.py --run-id "$PUBLIC_RUN_ID") - jq . <<< "$resolution" - echo "artifacts_json=$(jq -c .artifacts <<< "$resolution")" >> "$GITHUB_OUTPUT" - echo "correlation_id=$(jq -r .correlation_id <<< "$resolution")" >> "$GITHUB_OUTPUT" - echo "public_producer_jobs_json=$(jq -c .producer_jobs <<< "$resolution")" >> "$GITHUB_OUTPUT" - echo "public_run_attempt=$(jq -r .run_attempt <<< "$resolution")" >> "$GITHUB_OUTPUT" - echo "public_sha=$(jq -r .sha <<< "$resolution")" >> "$GITHUB_OUTPUT" - { - echo "Resolved public run \`$PUBLIC_RUN_ID\` attempt \`$(jq -r .run_attempt <<< "$resolution")\`." - echo - echo "Verified producers:" - jq -r '.producer_jobs[] | "- `\(.name)` (job `\(.id)`)"' <<< "$resolution" - echo - echo "Exact Windows Arm64 artifacts:" - jq -r '.artifacts[] | "- `\(.name)` (`\(.digest)`)"' <<< "$resolution" - } >> "$GITHUB_STEP_SUMMARY" - - dispatch-private-validation: - needs: resolve-public-build - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: {} - steps: - - name: Create private dispatch token - id: private-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} - private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} - owner: NVIDIA-dev - repositories: cuda-python-private - permission-actions: write - - - name: Dispatch and verify exact private validation - env: - ARTIFACTS_JSON: ${{ needs.resolve-public-build.outputs.artifacts-json }} - BASELINE_SHA: ${{ inputs.baseline_sha }} - COMMIT_COUNT: ${{ inputs.commit_count }} - CORRELATION_ID: ${{ needs.resolve-public-build.outputs.correlation-id }} - GH_TOKEN: ${{ steps.private-app-token.outputs.token }} - PUBLIC_PRODUCER_JOBS_JSON: ${{ needs.resolve-public-build.outputs.public-producer-jobs-json }} - PUBLIC_RUN_ATTEMPT: ${{ needs.resolve-public-build.outputs.public-run-attempt }} - PUBLIC_RUN_ID: ${{ inputs.public_run_id }} - PUBLIC_SHA: ${{ needs.resolve-public-build.outputs.public-sha }} - run: | - set -euo pipefail - payload=$(jq -n \ - --arg artifacts_json "$ARTIFACTS_JSON" \ - --arg baseline_sha "$BASELINE_SHA" \ - --arg commit_count "$COMMIT_COUNT" \ - --arg correlation_id "$CORRELATION_ID" \ - --arg producer_jobs_json "$PUBLIC_PRODUCER_JOBS_JSON" \ - --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ - --arg run_id "$PUBLIC_RUN_ID" \ - --arg sha "$PUBLIC_SHA" ' - { - ref: "ctk-next", - return_run_details: true, - inputs: { - schema_version: "2", - public_repository: "NVIDIA/cuda-python", - public_repository_id: "381173759", - public_workflow_id: "155304118", - public_producer_jobs_json: $producer_jobs_json, - public_run_id: $run_id, - public_run_attempt: $run_attempt, - public_sha: $sha, - artifacts_json: $artifacts_json, - baseline_sha: $baseline_sha, - commit_count: $commit_count, - correlation_id: $correlation_id - } - }') - response=$(gh api \ - --method POST \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ - --input - <<< "$payload") - private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") - private_run=$(gh api \ - -H 'X-GitHub-Api-Version: 2026-03-10' \ - "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") - jq -e \ - --arg title "WoA validation $CORRELATION_ID" ' - .repository.id == 809898190 and - .path == ".github/workflows/woa-main-validation.yml" and - .event == "workflow_dispatch" and - .head_branch == "ctk-next" and - .display_title == $title - ' <<< "$private_run" >/dev/null - echo "Exact private validation accepted the selected public batch." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/woa-private-dispatch.yml b/.github/workflows/woa-private-dispatch.yml index b4820f79f54..42ec2b728c4 100644 --- a/.github/workflows/woa-private-dispatch.yml +++ b/.github/workflows/woa-private-dispatch.yml @@ -6,6 +6,22 @@ name: "WoA exact public-main dispatch" run-name: "WoA exact dispatch for public run ${{ inputs.public_run_id }}" on: + workflow_call: + inputs: + public_run_id: + required: true + type: string + baseline_sha: + required: false + default: "" + type: string + commit_count: + required: false + default: "1" + type: string + secrets: + CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY: + required: true workflow_dispatch: inputs: public_run_id: @@ -16,14 +32,183 @@ on: permissions: {} jobs: - dispatch: + resolve-public-build: if: ${{ github.repository == 'NVIDIA/cuda-python' }} + runs-on: ubuntu-latest + timeout-minutes: 10 permissions: actions: read contents: read - uses: ./.github/workflows/woa-private-dispatch-reusable.yml - with: - public_run_id: ${{ inputs.public_run_id }} - baseline_sha: "" - commit_count: "1" - secrets: inherit + outputs: + artifacts-json: ${{ steps.resolve.outputs.artifacts_json }} + correlation-id: ${{ steps.resolve.outputs.correlation_id }} + public-producer-job-id: ${{ steps.resolve.outputs.public_producer_job_id }} + public-run-attempt: ${{ steps.resolve.outputs.public_run_attempt }} + public-sha: ${{ steps.resolve.outputs.public_sha }} + steps: + - name: Resolve exact public build + id: resolve + env: + GH_TOKEN: ${{ github.token }} + PUBLIC_RUN_ID: ${{ inputs.public_run_id }} + run: | + set -euo pipefail + [[ "$PUBLIC_RUN_ID" =~ ^[1-9][0-9]*$ ]] + run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID") + jq -e ' + .repository.id == 381173759 and + .workflow_id == 155304118 and + .path == ".github/workflows/ci.yml" and + .event == "push" and + .head_branch == "main" and + ((.status == "in_progress" and .conclusion == null) or + (.status == "completed" and .conclusion == "success")) and + (.head_sha | test("^[0-9a-f]{40}$")) + ' <<< "$run" >/dev/null + public_sha=$(jq -r .head_sha <<< "$run") + public_run_attempt=$(jq -r '.run_attempt | tostring' <<< "$run") + + comparison=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/compare/$public_sha...main") + jq -e '.status == "ahead" or .status == "identical"' <<< "$comparison" >/dev/null + + jobs=$(gh api --paginate \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID/attempts/$public_run_attempt/jobs?per_page=100" \ + --jq '.jobs') + producer_jobs=$(jq -s -c ' + add + | map(select(.name == "Build win-arm64, CUDA 13.4.2 / py3.13")) + ' <<< "$jobs") + jq -e ' + length == 1 and + .[0].status == "completed" and + .[0].conclusion == "success" + ' <<< "$producer_jobs" >/dev/null + public_producer_job_id=$(jq -r '.[0].id | tostring' <<< "$producer_jobs") + + expected_names=$(jq -nc --arg sha "$public_sha" ' + (["cuda-pathfinder-wheel"] + (["311", "312", "313", "314", "314t", "315", "315t"] | + map( + ("cuda-bindings-python" + . + "-cuda13.4.2-win-arm64-" + $sha), + ("cuda-core-python" + . + "-win-arm64-" + $sha) + ))) + ') + artifacts_json='[]' + while IFS= read -r artifact_name; do + response=$(gh api --method GET \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID/artifacts" \ + -f name="$artifact_name" \ + -F per_page=100) + artifact=$(jq -c -e \ + --arg name "$artifact_name" ' + if .total_count == 1 and + (.artifacts | length) == 1 and + .artifacts[0].name == $name and + .artifacts[0].expired == false + then .artifacts[0] | {id, name, digest} + else error("expected exactly one unexpired artifact named " + $name) + end + ' <<< "$response") + artifacts_json=$(jq -c \ + --argjson artifact "$artifact" \ + '. + [$artifact] | sort_by(.name)' <<< "$artifacts_json") + done < <(jq -r '.[]' <<< "$expected_names") + jq -e ' + length == 15 and + all(.[ ]; + (.id | type == "number") and + (.digest | test("^sha256:[0-9a-f]{64}$"))) + ' <<< "$artifacts_json" >/dev/null + + correlation_id="v1:381173759:$PUBLIC_RUN_ID:$public_run_attempt:$public_sha" + echo "artifacts_json=$artifacts_json" >> "$GITHUB_OUTPUT" + echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT" + echo "public_producer_job_id=$public_producer_job_id" >> "$GITHUB_OUTPUT" + echo "public_run_attempt=$public_run_attempt" >> "$GITHUB_OUTPUT" + echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT" + { + echo "Resolved public run \`$PUBLIC_RUN_ID\` attempt \`$public_run_attempt\` and producer job \`$public_producer_job_id\`." + echo "" + echo "Exact Windows Arm64 artifacts:" + jq -r '.[] | "- `\(.name)` (`\(.digest)`)"' <<< "$artifacts_json" + } >> "$GITHUB_STEP_SUMMARY" + + dispatch-private-validation: + needs: resolve-public-build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: {} + steps: + - name: Create private dispatch token + id: private-app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ vars.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_CLIENT_ID }} + private-key: ${{ secrets.CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY }} + owner: NVIDIA-dev + repositories: cuda-python-private + permission-actions: write + + - name: Dispatch and verify exact private validation + env: + ARTIFACTS_JSON: ${{ needs.resolve-public-build.outputs.artifacts-json }} + BASELINE_SHA: ${{ inputs.baseline_sha || '' }} + COMMIT_COUNT: ${{ inputs.commit_count || '1' }} + CORRELATION_ID: ${{ needs.resolve-public-build.outputs.correlation-id }} + GH_TOKEN: ${{ steps.private-app-token.outputs.token }} + PUBLIC_RUN_ATTEMPT: ${{ needs.resolve-public-build.outputs.public-run-attempt }} + PUBLIC_RUN_ID: ${{ inputs.public_run_id }} + PUBLIC_PRODUCER_JOB_ID: ${{ needs.resolve-public-build.outputs.public-producer-job-id }} + PUBLIC_SHA: ${{ needs.resolve-public-build.outputs.public-sha }} + run: | + set -euo pipefail + payload=$(jq -n \ + --arg artifacts_json "$ARTIFACTS_JSON" \ + --arg baseline_sha "$BASELINE_SHA" \ + --arg commit_count "$COMMIT_COUNT" \ + --arg correlation_id "$CORRELATION_ID" \ + --arg producer_job_id "$PUBLIC_PRODUCER_JOB_ID" \ + --arg run_attempt "$PUBLIC_RUN_ATTEMPT" \ + --arg run_id "$PUBLIC_RUN_ID" \ + --arg sha "$PUBLIC_SHA" \ + '{ + ref: "ctk-next", + return_run_details: true, + inputs: { + schema_version: "1", + public_repository: "NVIDIA/cuda-python", + public_repository_id: "381173759", + public_workflow_id: "155304118", + public_producer_job_id: $producer_job_id, + public_run_id: $run_id, + public_run_attempt: $run_attempt, + public_sha: $sha, + artifacts_json: $artifacts_json, + baseline_sha: $baseline_sha, + commit_count: $commit_count, + correlation_id: $correlation_id + } + }') + response=$(gh api \ + --method POST \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + repos/NVIDIA-dev/cuda-python-private/actions/workflows/woa-main-validation.yml/dispatches \ + --input - <<< "$payload") + private_run_id=$(jq -er '.workflow_run_id | tostring' <<< "$response") + run=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/NVIDIA-dev/cuda-python-private/actions/runs/$private_run_id") + jq -e \ + --arg title "WoA validation $CORRELATION_ID" ' + .repository.id == 809898190 and + .path == ".github/workflows/woa-main-validation.yml" and + .event == "workflow_dispatch" and + .head_branch == "ctk-next" and + .display_title == $title + ' <<< "$run" >/dev/null + echo "Private validation workflow accepted the exact dispatch." >> "$GITHUB_STEP_SUMMARY" diff --git a/ci/tools/woa_xrepo_resolve.py b/ci/tools/woa_xrepo_resolve.py deleted file mode 100644 index cb299af13dd..00000000000 --- a/ci/tools/woa_xrepo_resolve.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -import argparse -import http.client -import json -import os -import re -import sys -import urllib.parse - -PUBLIC_REPOSITORY = "NVIDIA/cuda-python" -PUBLIC_REPOSITORY_ID = 381173759 -PUBLIC_WORKFLOW_ID = 155304118 -PUBLIC_WORKFLOW_PATH = ".github/workflows/ci.yml" -REPORTING_APP_ID = 4954254 -CHECK_NAME = "cuda-python WoA integration" -CUDA_BUILD_VERSION = "13.4.2" -PYTHON_VERSIONS = ("3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t") - - -class GitHubAPI: - def __init__(self, token): - self._token = token - - def get(self, path, params=None): - request_path = f"/{path.lstrip('/')}" - if params: - request_path = f"{request_path}?{urllib.parse.urlencode(params)}" - connection = http.client.HTTPSConnection("api.github.com") - try: - connection.request( - "GET", - request_path, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {self._token}", - "X-GitHub-Api-Version": "2026-03-10", - "User-Agent": "cuda-python-woa-resolver", - }, - ) - response = connection.getresponse() - body = response.read().decode("utf-8", errors="replace") - if response.status >= 400: - raise RuntimeError(f"GitHub API GET {path} failed: {response.status}: {body}") - return json.loads(body) - finally: - connection.close() - - def paginate(self, path, item_key, params=None, max_pages=10): - items = [] - query = dict(params or {}) - query["per_page"] = 100 - for page in range(1, max_pages + 1): - query["page"] = page - response = self.get(path, query) - page_items = response[item_key] - items.extend(page_items) - if len(page_items) < 100: - break - return items - - -def expected_producer_names(): - names = [f"Build linux-64, CUDA {CUDA_BUILD_VERSION} / py3.10"] - names.extend(f"Build win-arm64, CUDA {CUDA_BUILD_VERSION} / py{version}" for version in PYTHON_VERSIONS) - return names - - -def expected_artifact_names(sha): - names = ["cuda-pathfinder-wheel"] - for version in PYTHON_VERSIONS: - python_tag = version.replace(".", "") - names.extend( - ( - f"cuda-bindings-python{python_tag}-cuda{CUDA_BUILD_VERSION}-win-arm64-{sha}", - f"cuda-core-python{python_tag}-win-arm64-{sha}", - ) - ) - return names - - -def select_artifacts(api, run_id, sha): - result = [] - for name in expected_artifact_names(sha): - response = api.get( - f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/artifacts", - {"name": name, "per_page": 100}, - ) - artifacts = response["artifacts"] - if response["total_count"] != 1 or len(artifacts) != 1: - return None - artifact = artifacts[0] - digest = artifact.get("digest") - if not ( - artifact["name"] == name - and artifact["expired"] is False - and isinstance(artifact["id"], int) - and artifact["id"] > 0 - and isinstance(digest, str) - and re.fullmatch(r"sha256:[0-9a-f]{64}", digest) - ): - return None - result.append({"id": artifact["id"], "name": artifact["name"], "digest": digest}) - return sorted(result, key=lambda item: item["name"]) - - -def run_is_admissible(run): - state_is_admissible = (run["status"] == "in_progress" and run["conclusion"] is None) or ( - run["status"] == "completed" and run["conclusion"] == "success" - ) - return ( - run["repository"]["id"] == PUBLIC_REPOSITORY_ID - and run["workflow_id"] == PUBLIC_WORKFLOW_ID - and run["path"] == PUBLIC_WORKFLOW_PATH - and run["event"] == "push" - and run["head_branch"] == "main" - and state_is_admissible - and re.fullmatch(r"[0-9a-f]{40}", run["head_sha"]) - ) - - -def validate_run(api, run): - if not run_is_admissible(run): - return None - - run_id = run["id"] - attempt = run["run_attempt"] - jobs = api.paginate( - f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}/attempts/{attempt}/jobs", - "jobs", - ) - producer_jobs = [] - for name in expected_producer_names(): - matches = [job for job in jobs if job["name"] == name] - if not (len(matches) == 1 and matches[0]["status"] == "completed" and matches[0]["conclusion"] == "success"): - return None - producer_jobs.append({"id": matches[0]["id"], "name": name}) - - selected_artifacts = select_artifacts(api, run_id, run["head_sha"]) - if selected_artifacts is None: - return None - - comparison = api.get(f"repos/{PUBLIC_REPOSITORY}/compare/{run['head_sha']}...main") - if comparison["status"] not in {"ahead", "identical"}: - return None - - return { - "run_id": str(run_id), - "run_attempt": str(attempt), - "producer_jobs": sorted(producer_jobs, key=lambda item: item["name"]), - "sha": run["head_sha"], - "artifacts": selected_artifacts, - "correlation_id": f"v1:{PUBLIC_REPOSITORY_ID}:{run_id}:{attempt}:{run['head_sha']}", - } - - -def resolve_candidate(api, run_id): - if run_id: - run = api.get(f"repos/{PUBLIC_REPOSITORY}/actions/runs/{run_id}") - return validate_run(api, run) - - runs = api.get( - f"repos/{PUBLIC_REPOSITORY}/actions/workflows/ci.yml/runs", - { - "branch": "main", - "event": "push", - "status": "completed", - "per_page": 30, - }, - )["workflow_runs"] - for run in runs: - candidate = validate_run(api, run) - if candidate is not None: - return candidate - return None - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--run-id", required=True) - args = parser.parse_args() - if not args.run_id.isdigit(): - parser.error("--run-id must be numeric") - - token = os.environ.get("GITHUB_TOKEN") - if not token: - raise SystemExit("GITHUB_TOKEN is required") - candidate = resolve_candidate(GitHubAPI(token), args.run_id) - if candidate is None: - raise RuntimeError("the requested public run is not ready for WoA validation") - print(json.dumps(candidate, separators=(",", ":"), sort_keys=True)) - - -if __name__ == "__main__": - try: - main() - except Exception as error: - print(f"error: {error}", file=sys.stderr) - raise diff --git a/ci/tools/woa_xrepo_select.py b/ci/tools/woa_xrepo_select.py deleted file mode 100644 index edab836b1a0..00000000000 --- a/ci/tools/woa_xrepo_select.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -import argparse -import datetime -import json -import os -import sys - -from woa_xrepo_resolve import ( - CHECK_NAME, - PUBLIC_REPOSITORY, - REPORTING_APP_ID, - GitHubAPI, - resolve_candidate, -) - -MAX_LEDGER_COMMITS = 100 - - -def parse_time(value): - return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) - - -def app_checks(api, sha): - response = api.get( - f"repos/{PUBLIC_REPOSITORY}/commits/{sha}/check-runs", - {"check_name": CHECK_NAME, "per_page": 100}, - ) - return [ - check - for check in response["check_runs"] - if check["name"] == CHECK_NAME and check["app"]["id"] == REPORTING_APP_ID - ] - - -def list_commits(api): - return api.get( - f"repos/{PUBLIC_REPOSITORY}/commits", - {"sha": "main", "per_page": MAX_LEDGER_COMMITS}, - ) - - -def select_batch(api, candidate, batch_size, max_wait_seconds, now): - candidate_checks = app_checks(api, candidate["sha"]) - external_id = f"woa:{candidate['correlation_id']}" - if any(check.get("external_id") == external_id for check in candidate_checks): - return {"dispatch": False, "reason": "candidate already has an App-owned attempt"} - - commits = list_commits(api) - try: - candidate_index = next(index for index, commit in enumerate(commits) if commit["sha"] == candidate["sha"]) - except StopIteration as error: - raise RuntimeError(f"candidate SHA was not found on the latest {MAX_LEDGER_COMMITS} main commits") from error - - baseline_index = None - baseline_sha = "" - for index in range(candidate_index + 1, len(commits)): - checks = app_checks(api, commits[index]["sha"]) - successful = [check for check in checks if check["status"] == "completed" and check["conclusion"] == "success"] - if successful: - baseline_index = index - baseline_sha = commits[index]["sha"] - break - - if baseline_index is None: - return { - "dispatch": True, - "reason": "bootstrap: no successful App-owned baseline exists in the bounded ledger", - "baseline_sha": "", - "commit_count": 1, - } - - commit_count = baseline_index - candidate_index - oldest_pending = commits[baseline_index - 1] - oldest_time = parse_time(oldest_pending["commit"]["committer"]["date"]) - age_seconds = max(0, int((now - oldest_time).total_seconds())) - if commit_count >= batch_size: - reason = f"batch threshold reached: {commit_count} commits" - dispatch = True - elif age_seconds >= max_wait_seconds: - reason = f"maximum wait reached: oldest pending commit is {age_seconds} seconds old" - dispatch = True - else: - reason = ( - f"deferred: {commit_count}/{batch_size} commits and oldest pending age " - f"{age_seconds}/{max_wait_seconds} seconds" - ) - dispatch = False - return { - "dispatch": dispatch, - "reason": reason, - "baseline_sha": baseline_sha, - "commit_count": commit_count, - } - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--run-id", default="") - parser.add_argument("--batch-size", type=int, default=3) - parser.add_argument("--max-wait-seconds", type=int, default=7200) - args = parser.parse_args() - if args.run_id and not args.run_id.isdigit(): - parser.error("--run-id must be numeric") - if args.batch_size < 1 or args.max_wait_seconds < 1: - parser.error("batch size and maximum wait must be positive") - - token = os.environ.get("GITHUB_TOKEN") - if not token: - raise SystemExit("GITHUB_TOKEN is required") - api = GitHubAPI(token) - candidate = resolve_candidate(api, args.run_id) - if candidate is None: - print(json.dumps({"dispatch": False, "reason": "no eligible public WoA build"})) - return - - selection = select_batch( - api, - candidate, - args.batch_size, - args.max_wait_seconds, - datetime.datetime.now(datetime.timezone.utc), - ) - selection.update(candidate) - print(json.dumps(selection, separators=(",", ":"), sort_keys=True)) - - -if __name__ == "__main__": - try: - main() - except Exception as error: - print(f"error: {error}", file=sys.stderr) - raise