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 new file mode 100644 index 00000000000..8a3b2d203ea --- /dev/null +++ b/.github/workflows/woa-main-automation.yml @@ -0,0 +1,150 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: "WoA public-main automation" + +on: + workflow_call: + inputs: + public_run_id: + required: true + type: string + 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 successful build + required: false + type: string + +permissions: {} + +jobs: + select: + if: ${{ github.repository == 'NVIDIA/cuda-python' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + checks: read + contents: read + concurrency: + group: cuda-python-woa-public-selector + cancel-in-progress: false + outputs: + baseline-sha: ${{ steps.select.outputs.baseline_sha }} + commit-count: ${{ steps.select.outputs.commit_count }} + dispatch: ${{ steps.select.outputs.dispatch }} + public-run-id: ${{ steps.select.outputs.public_run_id }} + steps: + - name: Select public batch + id: select + env: + GH_TOKEN: ${{ github.token }} + INPUT_RUN_ID: ${{ inputs.public_run_id }} + run: | + set -euo pipefail + 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 == "in_progress" and .conclusion == null) or + (.status == "completed" and .conclusion == "success")) + ' <<< "$run" >/dev/null + 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" + + checks=$(gh api \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "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 + + commits=$(gh api --method GET \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + 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.yml b/.github/workflows/woa-private-dispatch.yml index e01fd107243..42ec2b728c4 100644 --- a/.github/workflows/woa-private-dispatch.yml +++ b/.github/workflows/woa-private-dispatch.yml @@ -6,10 +6,26 @@ 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: - 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 @@ -23,7 +39,6 @@ jobs: permissions: actions: read contents: read - checks: read outputs: artifacts-json: ${{ steps.resolve.outputs.artifacts_json }} correlation-id: ${{ steps.resolve.outputs.correlation_id }} @@ -31,7 +46,7 @@ jobs: public-run-attempt: ${{ steps.resolve.outputs.public_run_attempt }} public-sha: ${{ steps.resolve.outputs.public_sha }} steps: - - name: Resolve exact successful public build + - name: Resolve exact public build id: resolve env: GH_TOKEN: ${{ github.token }} @@ -48,8 +63,8 @@ jobs: .path == ".github/workflows/ci.yml" and .event == "push" and .head_branch == "main" and - .status == "completed" and - .conclusion == "success" 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") @@ -75,12 +90,13 @@ jobs: ' <<< "$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) - ]') + 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 \ @@ -103,7 +119,7 @@ jobs: '. + [$artifact] | sort_by(.name)' <<< "$artifacts_json") done < <(jq -r '.[]' <<< "$expected_names") jq -e ' - length == 3 and + length == 15 and all(.[ ]; (.id | type == "number") and (.digest | test("^sha256:[0-9a-f]{64}$"))) @@ -141,6 +157,8 @@ jobs: - 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 }} @@ -151,6 +169,8 @@ jobs: 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" \ @@ -169,8 +189,8 @@ jobs: public_run_attempt: $run_attempt, public_sha: $sha, artifacts_json: $artifacts_json, - baseline_sha: "", - commit_count: "1", + baseline_sha: $baseline_sha, + commit_count: $commit_count, correlation_id: $correlation_id } }') @@ -183,10 +203,12 @@ jobs: 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 + 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"