Skip to content

Commit d1ea93a

Browse files
committed
Restore explicit build dependencies for WoA dispatch
1 parent 0b144bd commit d1ea93a

4 files changed

Lines changed: 83 additions & 190 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,25 @@ 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+
479498
# NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*)
480499
# so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by
481500
# build_hooks.py) are exercised on their target OS. Keep these job definitions

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

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

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

88
on:
9-
push:
10-
branches: [main]
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+
workflow_dispatch:
18+
inputs:
19+
public_run_id:
20+
description: Eligible public main CI run; leave empty to find the latest successful main CI run
21+
required: false
22+
type: string
1123

1224
permissions: {}
1325

@@ -17,9 +29,9 @@ concurrency:
1729

1830
jobs:
1931
resolve-public-build:
20-
if: ${{ github.repository == 'NVIDIA/cuda-python' }}
32+
if: ${{ github.repository == 'NVIDIA/cuda-python' && github.ref == 'refs/heads/main' }}
2133
runs-on: ubuntu-latest
22-
timeout-minutes: 120
34+
timeout-minutes: 10
2335
permissions:
2436
actions: read
2537
contents: read
@@ -36,57 +48,56 @@ jobs:
3648
ref: ${{ github.sha }}
3749
persist-credentials: false
3850
sparse-checkout: |
51+
ci/tools/lookup-run-id
3952
ci/tools/select-woa-batch
40-
ci/versions.yml
4153
sparse-checkout-cone-mode: false
4254

4355
- name: Resolve exact public build
4456
id: resolve
4557
env:
4658
GH_TOKEN: ${{ github.token }}
47-
PUBLIC_SHA: ${{ github.sha }}
59+
PUBLIC_RUN_ID: ${{ inputs.public_run_id }}
4860
run: |
4961
set -euo pipefail
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
62+
public_sha=''
6563
if [[ -z "$PUBLIC_RUN_ID" ]]; then
66-
echo "::error::No main push CI run appeared for $PUBLIC_SHA within ten minutes."
67-
exit 1
64+
lookup_output=$(./ci/tools/lookup-run-id --branch main --event push --head-sha NVIDIA/cuda-python CI)
65+
mapfile -t lookup_result <<< "$lookup_output"
66+
[[ ${#lookup_result[@]} == 2 ]]
67+
PUBLIC_RUN_ID=${lookup_result[0]}
68+
public_sha=${lookup_result[1]}
69+
[[ "$public_sha" =~ ^[0-9a-f]{40}$ ]]
6870
fi
6971
[[ "$PUBLIC_RUN_ID" =~ ^[1-9][0-9]*$ ]]
7072
run=$(gh api \
7173
-H 'X-GitHub-Api-Version: 2026-03-10' \
7274
"repos/NVIDIA/cuda-python/actions/runs/$PUBLIC_RUN_ID")
73-
jq -e --arg sha "$PUBLIC_SHA" '
75+
jq -e --arg sha "$public_sha" '
7476
.repository.full_name == "NVIDIA/cuda-python" and
7577
.workflow_id == 155304118 and
7678
.path == ".github/workflows/ci.yml" and
7779
.event == "push" and
7880
.head_branch == "main" and
79-
((.status != "completed" and .conclusion == null) or
81+
((.status == "in_progress" and .conclusion == null) or
8082
(.status == "completed" and .conclusion == "success")) and
81-
.head_sha == $sha and
83+
($sha == "" or .head_sha == $sha) and
8284
(.head_sha | test("^[0-9a-f]{40}$"))
8385
' <<< "$run" >/dev/null
84-
correlation_id="v1:NVIDIA/cuda-python:$PUBLIC_RUN_ID:$PUBLIC_SHA"
86+
if [[ -z "$public_sha" ]]; then
87+
public_sha=$(jq -r .head_sha <<< "$run")
88+
fi
89+
90+
comparison=$(gh api \
91+
-H 'X-GitHub-Api-Version: 2026-03-10' \
92+
"repos/NVIDIA/cuda-python/compare/$public_sha...main")
93+
jq -e '.status == "ahead" or .status == "identical"' <<< "$comparison" >/dev/null
94+
95+
correlation_id="v1:NVIDIA/cuda-python:$PUBLIC_RUN_ID:$public_sha"
8596
echo "correlation_id=$correlation_id" >> "$GITHUB_OUTPUT"
8697
echo "public_run_id=$PUBLIC_RUN_ID" >> "$GITHUB_OUTPUT"
87-
echo "public_sha=$PUBLIC_SHA" >> "$GITHUB_OUTPUT"
98+
echo "public_sha=$public_sha" >> "$GITHUB_OUTPUT"
8899
{
89-
echo "Resolved public run \`$PUBLIC_RUN_ID\` at commit \`$PUBLIC_SHA\`."
100+
echo "Resolved public run \`$PUBLIC_RUN_ID\` at commit \`$public_sha\`."
90101
} >> "$GITHUB_STEP_SUMMARY"
91102
92103
- name: Select public batch
@@ -97,53 +108,6 @@ jobs:
97108
PUBLIC_SHA: ${{ steps.resolve.outputs.public_sha }}
98109
run: bash ./ci/tools/select-woa-batch
99110

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-
147111
dispatch-private-validation:
148112
needs: resolve-public-build
149113
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-
# Each main push evaluates three commits or two hours; idle batches have no timer.
66+
# Each dispatch 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: 22 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,39 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
# ruff: noqa: S603
5-
6-
import json
7-
import os
8-
import shutil
9-
import subprocess
10-
import sys
114
from pathlib import Path
125

136
import pytest
147
import yaml
158

169
ROOT = Path(__file__).resolve().parents[3]
1710
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
8711

8812

8913
@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"]
14+
def test_entry_allows_reusable_and_manual_dispatch_without_cron():
15+
assert set(WORKFLOW[True]) == {"workflow_call", "workflow_dispatch"}
16+
assert WORKFLOW[True]["workflow_call"]["inputs"]["public_run_id"]["required"] is True
17+
assert WORKFLOW[True]["workflow_dispatch"]["inputs"]["public_run_id"]["required"] is False
18+
assert WORKFLOW["jobs"]["resolve-public-build"]["if"] == (
19+
"${{ github.repository == 'NVIDIA/cuda-python' && github.ref == 'refs/heads/main' }}"
20+
)
9421
assert WORKFLOW["concurrency"]["cancel-in-progress"] is False
9522

9623

9724
@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
25+
def test_ci_dispatch_requires_successful_builds_on_main_push():
26+
ci = yaml.safe_load((ROOT / ".github/workflows/ci.yml").read_text())
27+
dispatch = ci["jobs"]["woa-main-dispatch"]
28+
assert dispatch["needs"] == ["build-linux-64", "build-windows-arm64"]
29+
for condition in (
30+
"github.event_name == 'push'",
31+
"github.ref == 'refs/heads/main'",
32+
"needs.build-linux-64.result == 'success'",
33+
"needs.build-windows-arm64.result == 'success'",
34+
):
35+
assert condition in dispatch["if"]
36+
assert dispatch["uses"] == "./.github/workflows/woa-main-ci.yml"
37+
assert dispatch["with"]["public_run_id"] == "${{ github.run_id }}"
38+
steps = WORKFLOW["jobs"]["resolve-public-build"]["steps"]
39+
assert "Wait for public WoA wheel artifacts" not in {step["name"] for step in steps}

0 commit comments

Comments
 (0)