Skip to content

Commit 0b144bd

Browse files
committed
Run WoA orchestration independently on main pushes
1 parent 9db98f2 commit 0b144bd

4 files changed

Lines changed: 206 additions & 62 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -476,25 +476,6 @@ jobs:
476476
single-cuda-major: ${{ needs.ci-vars.outputs.WINDOWS_ARM64_SINGLE_CUDA_MAJOR == 'true' }}
477477
python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }}
478478

479-
woa-main-dispatch:
480-
name: Dispatch WoA integration
481-
needs:
482-
- build-linux-64
483-
- build-windows-arm64
484-
if: ${{ github.repository_owner == 'nvidia' &&
485-
github.event_name == 'push' &&
486-
github.ref == 'refs/heads/main' &&
487-
needs.build-linux-64.result == 'success' &&
488-
needs.build-windows-arm64.result == 'success' }}
489-
permissions:
490-
actions: read
491-
checks: read
492-
contents: read
493-
uses: ./.github/workflows/woa-main-ci.yml
494-
with:
495-
public_run_id: ${{ github.run_id }}
496-
secrets: inherit
497-
498479
# NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*)
499480
# so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by
500481
# build_hooks.py) are exercised on their target OS. Keep these job definitions

‎.github/workflows/woa-main-ci.yml‎

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,11 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
name: "WoA main CI"
6-
run-name: "WoA main CI for ${{ inputs.public_run_id || 'latest successful public main CI run' }}"
6+
run-name: "WoA main CI for ${{ github.sha }}"
77

88
on:
9-
workflow_call:
10-
inputs:
11-
public_run_id:
12-
required: true
13-
type: string
14-
secrets:
15-
CUDA_PYTHON_WOA_XREPO_CI_PRIVATE_APP_PRIVATE_KEY:
16-
required: true
17-
schedule:
18-
- cron: "17 * * * *"
19-
workflow_dispatch:
20-
inputs:
21-
public_run_id:
22-
description: Eligible public main CI run; leave empty to find the latest successful main CI run
23-
required: false
24-
type: string
9+
push:
10+
branches: [main]
2511

2612
permissions: {}
2713

@@ -33,7 +19,7 @@ jobs:
3319
resolve-public-build:
3420
if: ${{ github.repository == 'NVIDIA/cuda-python' }}
3521
runs-on: ubuntu-latest
36-
timeout-minutes: 10
22+
timeout-minutes: 120
3723
permissions:
3824
actions: read
3925
contents: read
@@ -50,56 +36,57 @@ jobs:
5036
ref: ${{ github.sha }}
5137
persist-credentials: false
5238
sparse-checkout: |
53-
ci/tools/lookup-run-id
5439
ci/tools/select-woa-batch
40+
ci/versions.yml
5541
sparse-checkout-cone-mode: false
5642

5743
- name: Resolve exact public build
5844
id: resolve
5945
env:
6046
GH_TOKEN: ${{ github.token }}
61-
PUBLIC_RUN_ID: ${{ inputs.public_run_id }}
47+
PUBLIC_SHA: ${{ github.sha }}
6248
run: |
6349
set -euo pipefail
64-
public_sha=''
50+
PUBLIC_RUN_ID=''
51+
for ((attempt = 0; attempt < 20; attempt++)); do
52+
runs=$(gh api --method GET \
53+
-H 'X-GitHub-Api-Version: 2026-03-10' \
54+
repos/NVIDIA/cuda-python/actions/workflows/ci.yml/runs \
55+
-f event=push -f branch=main -f head_sha="$PUBLIC_SHA" -F per_page=100)
56+
PUBLIC_RUN_ID=$(jq -r --arg sha "$PUBLIC_SHA" '
57+
[.workflow_runs[] | select(.event == "push" and
58+
.head_branch == "main" and .head_sha == $sha)]
59+
| max_by(.id) | .id // empty
60+
' <<< "$runs")
61+
[[ -z "$PUBLIC_RUN_ID" ]] || break
62+
echo "Waiting for main push CI run at $PUBLIC_SHA (attempt $((attempt + 1))/20)."
63+
sleep 30
64+
done
6565
if [[ -z "$PUBLIC_RUN_ID" ]]; then
66-
lookup_output=$(./ci/tools/lookup-run-id --branch main --event push --head-sha NVIDIA/cuda-python CI)
67-
mapfile -t lookup_result <<< "$lookup_output"
68-
[[ ${#lookup_result[@]} == 2 ]]
69-
PUBLIC_RUN_ID=${lookup_result[0]}
70-
public_sha=${lookup_result[1]}
71-
[[ "$public_sha" =~ ^[0-9a-f]{40}$ ]]
66+
echo "::error::No main push CI run appeared for $PUBLIC_SHA within ten minutes."
67+
exit 1
7268
fi
7369
[[ "$PUBLIC_RUN_ID" =~ ^[1-9][0-9]*$ ]]
7470
run=$(gh api \
7571
-H 'X-GitHub-Api-Version: 2026-03-10' \
7672
"repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID")
77-
jq -e --arg sha "$public_sha" '
73+
jq -e --arg sha "$PUBLIC_SHA" '
7874
.repository.full_name == "NVIDIA/cuda-python" and
7975
.workflow_id == 155304118 and
8076
.path == ".github/workflows/ci.yml" and
8177
.event == "push" and
8278
.head_branch == "main" and
83-
((.status == "in_progress" and .conclusion == null) or
79+
((.status != "completed" and .conclusion == null) or
8480
(.status == "completed" and .conclusion == "success")) and
85-
($sha == "" or .head_sha == $sha) and
81+
.head_sha == $sha and
8682
(.head_sha | test("^[0-9a-f]{40}$"))
8783
' <<< "$run" >/dev/null
88-
if [[ -z "$public_sha" ]]; then
89-
public_sha=$(jq -r .head_sha <<< "$run")
90-
fi
91-
92-
comparison=$(gh api \
93-
-H 'X-GitHub-Api-Version: 2026-03-10' \
94-
"repos/NVIDIA/cuda-python/compare/$public_sha...main")
95-
jq -e '.status == "ahead" or .status == "identical"' <<< "$comparison" >/dev/null
96-
97-
correlation_id="v1:NVIDIA/cuda-python:$PUBLIC_RUN_ID:$public_sha"
84+
correlation_id="v1:NVIDIA/cuda-python:$PUBLIC_RUN_ID:$PUBLIC_SHA"
9885
echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT"
9986
echo "public_run_id=$PUBLIC_RUN_ID" >> "$GITHUB_OUTPUT"
100-
echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT"
87+
echo "public_sha=$PUBLIC_SHA" >> "$GITHUB_OUTPUT"
10188
{
102-
echo "Resolved public run \`$PUBLIC_RUN_ID\` at commit \`$public_sha\`."
89+
echo "Resolved public run \`$PUBLIC_RUN_ID\` at commit \`$PUBLIC_SHA\`."
10390
} >> "$GITHUB_STEP_SUMMARY"
10491
10592
- name: Select public batch
@@ -110,6 +97,53 @@ jobs:
11097
PUBLIC_SHA: ${{ steps.resolve.outputs.public_sha }}
11198
run: bash ./ci/tools/select-woa-batch
11299

100+
- name: Wait for public WoA wheel artifacts
101+
if: ${{ steps.select.outputs.dispatch == 'true' }}
102+
timeout-minutes: 100
103+
env:
104+
GH_TOKEN: ${{ github.token }}
105+
PUBLIC_RUN_ID: ${{ steps.resolve.outputs.public_run_id }}
106+
PUBLIC_SHA: ${{ steps.resolve.outputs.public_sha }}
107+
run: |
108+
set -euo pipefail
109+
cuda_version=$(yq -r '.cuda.build.version' ci/versions.yml)
110+
mapfile -t python_versions < <(yq -r '.python.supported[]' ci/versions.yml)
111+
pending=(cuda-pathfinder-wheel)
112+
for version in "${python_versions[@]}"; do
113+
python_tag=${version//./}
114+
pending+=("cuda-bindings-python$python_tag-cuda$cuda_version-win-arm64-$PUBLIC_SHA"
115+
"cuda-core-python$python_tag-win-arm64-$PUBLIC_SHA")
116+
done
117+
while (( ${#pending[@]} > 0 )); do
118+
run=$(gh api \
119+
-H 'X-GitHub-Api-Version: 2026-03-10' \
120+
"repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID")
121+
if jq -e '.status == "completed" and .conclusion != "success"' <<< "$run" >/dev/null; then
122+
echo "::error::Public CI run $PUBLIC_RUN_ID ended with $(jq -r .conclusion <<< "$run")."
123+
exit 1
124+
fi
125+
artifact_name=${pending[0]}
126+
artifacts=$(gh api --method GET \
127+
-H 'X-GitHub-Api-Version: 2026-03-10' \
128+
"repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID/artifacts" \
129+
-f name="$artifact_name")
130+
if jq -e --arg name "$artifact_name" '
131+
.total_count == 1 and (.artifacts | length == 1) and
132+
.artifacts[0].name == $name and .artifacts[0].expired == false
133+
' <<< "$artifacts" >/dev/null; then
134+
echo "Ready: $artifact_name"
135+
pending=("${pending[@]:1}")
136+
elif [[ $(jq -r .status <<< "$run") == completed ]]; then
137+
echo "::error::Successful public CI run $PUBLIC_RUN_ID is missing usable artifact $artifact_name."
138+
jq . <<< "$artifacts"
139+
exit 1
140+
else
141+
echo "Waiting for $artifact_name (${#pending[@]} artifacts remaining)."
142+
sleep 60
143+
fi
144+
done
145+
echo "All public WoA wheel artifacts are ready; remaining public CI jobs need not finish." >> "$GITHUB_STEP_SUMMARY"
146+
113147
dispatch-private-validation:
114148
needs: resolve-public-build
115149
if: ${{ needs.resolve-public-build.outputs.dispatch == 'true' }}

‎ci/tools/select-woa-batch‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ if [[ -n "$baseline_index" ]]; then
6363
'.[$index].commit.committer.date' <<< "$commits")
6464
# Age starts at the oldest pending commit's timestamp, not workflow start time.
6565
age_seconds=$(($(date +%s) - $(date -d "$oldest_pending_at" +%s)))
66-
# Dispatch after three commits or two hours; hourly cron revisits deferred batches.
66+
# Each main push evaluates three commits or two hours; idle batches have no timer.
6767
if (( commit_count >= 3 )); then
6868
reason="Batch threshold reached: $commit_count commits."
6969
elif (( age_seconds >= 7200 )); then

‎ci/tools/tests/test_woa_main_ci.py‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# ruff: noqa: S603
5+
6+
import json
7+
import os
8+
import shutil
9+
import subprocess
10+
import sys
11+
from pathlib import Path
12+
13+
import pytest
14+
import yaml
15+
16+
ROOT = Path(__file__).resolve().parents[3]
17+
WORKFLOW = yaml.safe_load((ROOT / ".github/workflows/woa-main-ci.yml").read_text())
18+
STEPS = WORKFLOW["jobs"]["resolve-public-build"]["steps"]
19+
SHA = "a" * 40
20+
21+
22+
@pytest.fixture
23+
def run_step(tmp_path):
24+
mock = tmp_path / "gh"
25+
mock.write_text(
26+
f"#!{sys.executable}\n"
27+
"import json, os, sys\n"
28+
"from pathlib import Path\n"
29+
"args = sys.argv[1:]\n"
30+
"endpoint = next(arg for arg in args if arg.startswith('repos/'))\n"
31+
"run = json.loads(os.environ['RUN'])\n"
32+
"if endpoint.endswith('/ci.yml/runs'):\n"
33+
" print(json.dumps({'workflow_runs': [dict(run, id=999, event='schedule'), "
34+
"dict(run, id=998, head_sha='b' * 40), run]}))\n"
35+
"elif endpoint.endswith('/artifacts'):\n"
36+
" name = next(arg[5:] for arg in args if arg.startswith('name='))\n"
37+
" with Path(os.environ['REQUESTS']).open('a') as output:\n"
38+
" output.write(name + '\\n')\n"
39+
" ready = os.environ['MODE'] != 'missing' and "
40+
"(os.environ['MODE'] != 'delayed' or len(Path(os.environ['REQUESTS']).read_text().splitlines()) > 1)\n"
41+
" print(json.dumps({'total_count': int(ready), 'artifacts': "
42+
"[{'name': name, 'expired': False}] if ready else []}))\n"
43+
"else:\n"
44+
" print(json.dumps(run))\n"
45+
)
46+
mock.chmod(0o755)
47+
for name, body in {
48+
"sleep": "exit 0",
49+
"yq": "case $* in *supported*) echo 3.11; echo 3.14t;; *) echo 13.4.2;; esac",
50+
}.items():
51+
executable = tmp_path / name
52+
executable.write_text(f"#!/bin/bash\n{body}\n")
53+
executable.chmod(0o755)
54+
55+
def invoke(name, *, mode="ready", status="in_progress", conclusion=None):
56+
run = {
57+
"id": 123,
58+
"repository": {"full_name": "NVIDIA/cuda-python"},
59+
"workflow_id": 155304118,
60+
"path": ".github/workflows/ci.yml",
61+
"event": "push",
62+
"head_branch": "main",
63+
"head_sha": SHA,
64+
"status": status,
65+
"conclusion": conclusion,
66+
}
67+
result = subprocess.run(
68+
[shutil.which("bash"), "-c", next(step["run"] for step in STEPS if step["name"] == name)],
69+
env=dict(
70+
os.environ,
71+
PATH=str(tmp_path) + os.pathsep + os.environ["PATH"],
72+
PUBLIC_RUN_ID="123",
73+
PUBLIC_SHA=SHA,
74+
RUN=json.dumps(run),
75+
MODE=mode,
76+
REQUESTS=str(tmp_path / "requests"),
77+
GITHUB_OUTPUT=str(tmp_path / "outputs"),
78+
GITHUB_STEP_SUMMARY=str(tmp_path / "summary"),
79+
),
80+
capture_output=True,
81+
text=True,
82+
timeout=10,
83+
)
84+
return result, tmp_path
85+
86+
return invoke
87+
88+
89+
@pytest.mark.agent_authored(model="gpt-6")
90+
def test_entry_is_main_push_only():
91+
assert WORKFLOW[True] == {"push": {"branches": ["main"]}}
92+
ci = yaml.safe_load((ROOT / ".github/workflows/ci.yml").read_text())
93+
assert "woa-main-dispatch" not in ci["jobs"]
94+
assert WORKFLOW["concurrency"]["cancel-in-progress"] is False
95+
96+
97+
@pytest.mark.agent_authored(model="gpt-6")
98+
def test_resolver_selects_exact_push_sha_not_nightly(run_step):
99+
result, directory = run_step("Resolve exact public build", status="queued")
100+
assert result.returncode == 0, result.stderr
101+
assert "public_run_id=123\n" in (directory / "outputs").read_text()
102+
assert f"public_sha={SHA}\n" in (directory / "outputs").read_text()
103+
104+
105+
@pytest.mark.agent_authored(model="gpt-6")
106+
@pytest.mark.parametrize("mode", ["ready", "delayed"])
107+
def test_waiter_accepts_ready_wheels_before_ci_finishes(run_step, mode):
108+
result, directory = run_step("Wait for public WoA wheel artifacts", mode=mode)
109+
assert result.returncode == 0, result.stderr
110+
expected = [
111+
"cuda-pathfinder-wheel",
112+
f"cuda-bindings-python311-cuda13.4.2-win-arm64-{SHA}",
113+
f"cuda-core-python311-win-arm64-{SHA}",
114+
f"cuda-bindings-python314t-cuda13.4.2-win-arm64-{SHA}",
115+
f"cuda-core-python314t-win-arm64-{SHA}",
116+
]
117+
if mode == "delayed":
118+
expected.insert(0, "cuda-pathfinder-wheel")
119+
assert (directory / "requests").read_text().splitlines() == expected
120+
121+
122+
@pytest.mark.agent_authored(model="gpt-6")
123+
@pytest.mark.parametrize("conclusion", ["failure", "cancelled", "success"])
124+
def test_waiter_rejects_unsuccessful_ci_or_missing_wheels(run_step, conclusion):
125+
result, _ = run_step(
126+
"Wait for public WoA wheel artifacts", mode="missing", status="completed", conclusion=conclusion
127+
)
128+
assert result.returncode != 0
129+
assert "::error::" in result.stdout

0 commit comments

Comments
 (0)