diff --git a/.github/ISSUE_TEMPLATE/release_checklist.yml b/.github/ISSUE_TEMPLATE/release_checklist.yml index f7307fbe92..a5b5e1cd73 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.yml +++ b/.github/ISSUE_TEMPLATE/release_checklist.yml @@ -23,7 +23,7 @@ body: - label: "Finalize the doc update, including release notes (\"Note: Touching docstrings/type annotations in code is OK during code freeze, apply your best judgement!\")" - label: Update the docs for the new version - label: Create a public release tag - - label: Wait for the tag-triggered CI run to complete, and use that run ID for release workflows + - label: Confirm the tagged commit already has a green CI run on main, then run the release workflow for that tag - label: If any code change happens, rebuild the wheels from the new tag - label: Upload wheels to PyPI - label: Update the conda recipe & release conda packages diff --git a/.github/RELEASE-core.md b/.github/RELEASE-core.md index 01e182c76e..7ad8130db7 100644 --- a/.github/RELEASE-core.md +++ b/.github/RELEASE-core.md @@ -108,16 +108,14 @@ git push origin cuda-core-v0.6.0 --- -## Wait for the tag-triggered CI run to complete +## Verify the tagged commit already passed CI on `main` -Pushing the tag triggers a CI run automatically. Monitor it in the -**Actions** tab on GitHub. +Before running release, confirm that the tagged commit already has a +successful `CI` run on `main`. The release workflow now checks this as a +preflight step, but it is still worth verifying up front. -- **All CI tests should succeed.** If any fail, investigate and rerun as - needed. -- Note the **run ID** of the successful tag-triggered run. The release - workflow can auto-detect it from the tag, but you can also provide it - explicitly. +- **All CI tests on `main` should already be green.** If any fail, fix them + before tagging or release will be blocked. --- @@ -129,9 +127,8 @@ publish to TestPyPI, then publish the same wheel set to PyPI. 1. Go to **Actions > CI: Release** and run the workflow with: - **Component**: `cuda-core` - **The release git tag**: `cuda-core-v0.6.0` - - The workflow automatically looks up the successful tag-triggered CI run - for the selected release tag. + The workflow builds the release artifacts directly from the selected tag + after verifying that the tagged commit already passed `CI` on `main`. 2. Wait for the workflow to complete. It will: - publish the selected wheels to TestPyPI diff --git a/.github/workflows/build-cuda-core.yml b/.github/workflows/build-cuda-core.yml new file mode 100644 index 0000000000..5ae71e828a --- /dev/null +++ b/.github/workflows/build-cuda-core.yml @@ -0,0 +1,443 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Reusable workflow: build cuda.core wheels for one platform. +# +# cuda.core ships a single wheel that supports two CUDA major versions. The +# two builds (current and previous CUDA) run in parallel; a final merge job +# combines them into the artifact consumed by test and release jobs. +# +# All routing lives in the caller's job graph. This workflow does exactly: +# checkout -> sccache -> CTK -> download bindings -> cibuildwheel -> upload +# for each CUDA version, then merges. + +on: + workflow_call: + inputs: + host-platform: + required: true + type: string + cuda-version: + description: "Current CUDA version (e.g. 12.9.0)" + required: true + type: string + prev-cuda-version: + description: "Previous CUDA major version (e.g. 12.8.0)" + required: true + type: string + bindings-run-id: + description: "Run ID that produced the cuda-bindings artifacts for the current CUDA version" + required: true + type: string + prev-bindings-run-id: + description: "Run ID that produced the cuda-bindings artifacts for the previous CUDA version (defaults to bindings-run-id)" + required: false + default: "" + type: string + git-ref: + description: "Git ref to check out (defaults to the caller's ref)" + required: false + default: "" + type: string + +defaults: + run: + shell: bash --noprofile --norc -xeuo pipefail {0} + +permissions: + contents: read + +jobs: + # --------------------------------------------------------------------------- + # Build against the current CUDA major version. + # --------------------------------------------------------------------------- + build-current: + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "3.14t" + name: current-cuda py${{ matrix.python-version }} + runs-on: ${{ (inputs.host-platform == 'linux-64' && 'linux-amd64-cpu8') || + (inputs.host-platform == 'linux-aarch64' && 'linux-arm64-cpu8') || + (inputs.host-platform == 'win-64' && 'windows-2022') }} + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.git-ref || github.ref }} + + - name: Capture checked-out commit SHA + run: echo "CHECKED_OUT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" + + - name: Enable sccache + uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 + + - name: Add additional GHA cache-related env vars + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL']) + core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL']) + + - name: Setup proxy cache + uses: nv-gha-runners/setup-proxy-cache@main + continue-on-error: true + if: ${{ inputs.host-platform != 'win-64' }} + with: + enable-apt: true + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Set up MSVC + if: ${{ startsWith(inputs.host-platform, 'win') }} + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set up yq (Windows only) + if: ${{ startsWith(inputs.host-platform, 'win') }} + env: + YQ_URL: https://github.com/mikefarah/yq/releases/latest/download/yq_windows_amd64.exe + YQ_DIR: yq_latest + shell: pwsh -command ". '{0}'" + run: | + mkdir -Force -ErrorAction SilentlyContinue "${env:YQ_DIR}" | Out-Null + Invoke-WebRequest -UseBasicParsing -OutFile "${env:YQ_DIR}/yq.exe" -Uri "$env:YQ_URL" + echo "$((Get-Location).Path)\\$env:YQ_DIR" >> $env:GITHUB_PATH + $env:Path += ";$((Get-Location).Path)\\$env:YQ_DIR" + yq --version + + - name: Set environment variables + env: + CUDA_VER: ${{ inputs.cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + PY_VER: ${{ matrix.python-version }} + SHA: ${{ env.CHECKED_OUT_SHA }} + run: | + ./ci/tools/env-vars build + echo "SCCACHE_DIR=${HOME}/.cache/sccache" >> "${GITHUB_ENV}" + echo "SCCACHE_CACHE_SIZE=1G" >> "${GITHUB_ENV}" + + - name: Dump environment + run: env + + - name: Set up mini CTK (current CUDA) + uses: ./.github/actions/fetch_ctk + with: + host-platform: ${{ inputs.host-platform }} + cuda-version: ${{ inputs.cuda-version }} + + - name: Download cuda.bindings artifacts (current CUDA) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${{ inputs.cuda-version }}-${{ inputs.host-platform }}" + gh run download "${{ inputs.bindings-run-id }}" \ + -p "${BASENAME}*" \ + -R "${{ github.repository }}" + mkdir -p "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" + find ${BASENAME} -name "*.whl" -exec mv {} "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/" \; + rm -rf ${BASENAME} + + - name: Build cuda.core wheel (current CUDA) + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 + with: + package-dir: ./cuda_core/ + output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + env: + CIBW_BUILD: ${{ env.CIBW_BUILD }} + CIBW_ENVIRONMENT_LINUX: > + CUDA_PATH=/host/${{ env.CUDA_PATH }} + CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} + CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + PIP_FIND_LINKS=/host/${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + CC="/host/${{ env.SCCACHE_PATH }} cc" + CXX="/host/${{ env.SCCACHE_PATH }} c++" + SCCACHE_GHA_ENABLED=true + ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} + ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} + ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} + ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} + SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} + CIBW_ENVIRONMENT_WINDOWS: > + CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" + CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} + CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" + CIBW_BEFORE_TEST_LINUX: > + "/host${{ env.SCCACHE_PATH }}" --show-stats + CIBW_TEST_COMMAND: > + echo "ok!" + + - name: Rename and stage current-CUDA wheel + run: | + if [[ "${{ inputs.host-platform }}" == win* ]]; then + chown -R "$(whoami)" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + else + sudo chown -R "$(whoami)" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + fi + mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" + for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/*.whl; do + [[ -f "${wheel}" ]] || continue + base=$(basename "${wheel}" .whl) + mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}/${base}.cu${BUILD_CUDA_MAJOR}.whl" + done + ls -lahR "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + + - name: Upload current-CUDA cuda.core wheel + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-cu${{ env.BUILD_CUDA_MAJOR }} + path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${{ env.BUILD_CUDA_MAJOR }}/*.whl + if-no-files-found: error + + # --------------------------------------------------------------------------- + # Build against the previous CUDA major version. + # --------------------------------------------------------------------------- + build-prev: + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "3.14t" + name: prev-cuda py${{ matrix.python-version }} + runs-on: ${{ (inputs.host-platform == 'linux-64' && 'linux-amd64-cpu8') || + (inputs.host-platform == 'linux-aarch64' && 'linux-arm64-cpu8') || + (inputs.host-platform == 'win-64' && 'windows-2022') }} + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.git-ref || github.ref }} + + - name: Capture checked-out commit SHA + run: echo "CHECKED_OUT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" + + - name: Enable sccache + uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 + + - name: Add additional GHA cache-related env vars + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL']) + core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL']) + + - name: Setup proxy cache + uses: nv-gha-runners/setup-proxy-cache@main + continue-on-error: true + if: ${{ inputs.host-platform != 'win-64' }} + with: + enable-apt: true + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Set up MSVC + if: ${{ startsWith(inputs.host-platform, 'win') }} + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set up yq (Windows only) + if: ${{ startsWith(inputs.host-platform, 'win') }} + env: + YQ_URL: https://github.com/mikefarah/yq/releases/latest/download/yq_windows_amd64.exe + YQ_DIR: yq_latest + shell: pwsh -command ". '{0}'" + run: | + mkdir -Force -ErrorAction SilentlyContinue "${env:YQ_DIR}" | Out-Null + Invoke-WebRequest -UseBasicParsing -OutFile "${env:YQ_DIR}/yq.exe" -Uri "$env:YQ_URL" + echo "$((Get-Location).Path)\\$env:YQ_DIR" >> $env:GITHUB_PATH + $env:Path += ";$((Get-Location).Path)\\$env:YQ_DIR" + yq --version + + - name: Set environment variables + env: + # Use prev-cuda-version so CUDA_BINDINGS_ARTIFACT_BASENAME references the right CTK. + CUDA_VER: ${{ inputs.prev-cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + PY_VER: ${{ matrix.python-version }} + SHA: ${{ env.CHECKED_OUT_SHA }} + run: | + ./ci/tools/env-vars build + echo "SCCACHE_DIR=${HOME}/.cache/sccache" >> "${GITHUB_ENV}" + echo "SCCACHE_CACHE_SIZE=1G" >> "${GITHUB_ENV}" + + - name: Dump environment + run: env + + - name: Set up mini CTK (prev CUDA) + uses: ./.github/actions/fetch_ctk + with: + host-platform: ${{ inputs.host-platform }} + cuda-version: ${{ inputs.prev-cuda-version }} + cuda-path: "./cuda_toolkit_prev" + + - name: Download cuda.bindings artifacts (prev CUDA) + env: + GH_TOKEN: ${{ github.token }} + run: | + PREV_RUN_ID="${{ inputs.prev-bindings-run-id || inputs.bindings-run-id }}" + BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${{ inputs.prev-cuda-version }}-${{ inputs.host-platform }}" + + if [[ -z "${{ inputs.prev-bindings-run-id }}" ]]; then + # prev CUDA bindings come from the backport branch + OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) + PREV_RUN_ID=$(gh run list \ + -b "${OLD_BRANCH}" -L 1 -w "ci.yml" -s success \ + -R "${{ github.repository }}" \ + --json databaseId | jq -r '.[0].databaseId // empty') + if [[ -z "${PREV_RUN_ID}" ]]; then + echo "No successful CI run found on backport branch ${OLD_BRANCH}" >&2 + exit 1 + fi + fi + + gh run download "${PREV_RUN_ID}" \ + -p "${BASENAME}*" \ + -R "${{ github.repository }}" + mkdir -p "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" + find ${BASENAME} -name "*.whl" -exec mv {} "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/" \; + rm -rf ${BASENAME} + + - name: Build cuda.core wheel (prev CUDA) + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 + with: + package-dir: ./cuda_core/ + output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + env: + CIBW_BUILD: ${{ env.CIBW_BUILD }} + CIBW_ENVIRONMENT_LINUX: > + CUDA_PATH=/host/${{ env.CUDA_PATH }} + CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} + CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + PIP_FIND_LINKS=/host/${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + CC="/host/${{ env.SCCACHE_PATH }} cc" + CXX="/host/${{ env.SCCACHE_PATH }} c++" + SCCACHE_GHA_ENABLED=true + ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} + ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} + ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} + ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} + SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} + CIBW_ENVIRONMENT_WINDOWS: > + CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" + CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} + CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" + CIBW_BEFORE_TEST_LINUX: > + "/host${{ env.SCCACHE_PATH }}" --show-stats + CIBW_TEST_COMMAND: > + echo "ok!" + + - name: Rename and stage prev-CUDA wheel + run: | + if [[ "${{ inputs.host-platform }}" == win* ]]; then + chown -R "$(whoami)" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + else + sudo chown -R "$(whoami)" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + fi + mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" + for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/*.whl; do + [[ -f "${wheel}" ]] || continue + base=$(basename "${wheel}" .whl) + mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}/${base}.cu${BUILD_CUDA_MAJOR}.whl" + done + ls -lahR "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + + - name: Upload prev-CUDA cuda.core wheel + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-cu${{ env.BUILD_CUDA_MAJOR }} + path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${{ env.BUILD_CUDA_MAJOR }}/*.whl + if-no-files-found: error + + # --------------------------------------------------------------------------- + # Merge the two per-CUDA wheels into the final multi-CUDA artifact. + # --------------------------------------------------------------------------- + merge: + needs: [build-current, build-prev] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "3.14t" + name: merge py${{ matrix.python-version }} + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.git-ref || github.ref }} + + - name: Capture checked-out commit SHA + run: echo "CHECKED_OUT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" + + - name: Set environment variables + env: + CUDA_VER: ${{ inputs.cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + PY_VER: ${{ matrix.python-version }} + SHA: ${{ env.CHECKED_OUT_SHA }} + run: ./ci/tools/env-vars build + + - name: Download current-CUDA wheel + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-cu${{ env.BUILD_CUDA_MAJOR }} + path: current-cu + + - name: Download prev-CUDA wheel + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-cu${{ env.BUILD_PREV_CUDA_MAJOR }} + path: prev-cu + + - name: Set up Python for merge script + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Merge cuda.core wheels + run: | + pip install wheel twine + mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + python ci/tools/merge_cuda_core_wheels.py \ + current-cu/cuda_core*.whl \ + prev-cu/cuda_core*.whl \ + --output-dir "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + twine check --strict "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/*.whl + ls -lahR "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" + + - name: Upload merged cuda.core artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} + path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl + if-no-files-found: error diff --git a/.github/workflows/build-cython-tests.yml b/.github/workflows/build-cython-tests.yml new file mode 100644 index 0000000000..d36ae643c2 --- /dev/null +++ b/.github/workflows/build-cython-tests.yml @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Reusable workflow: build Cython test extensions for cuda.bindings and cuda.core. +# +# Runs after wheel artifacts exist. Steps: checkout -> download wheels -> +# setup python -> build Cython tests -> upload. No CTK, no cibuildwheel. + +on: + workflow_call: + inputs: + host-platform: + required: true + type: string + cuda-version: + description: "CUDA version the bindings were built against" + required: true + type: string + build-run-id: + description: "Run ID that produced the wheel artifacts" + required: true + type: string + git-ref: + description: "Git ref to check out (defaults to the caller's ref)" + required: false + default: "" + type: string + +defaults: + run: + shell: bash --noprofile --norc -xeuo pipefail {0} + +permissions: + contents: read + +jobs: + build-tests: + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "3.14t" + name: py${{ matrix.python-version }} + runs-on: ${{ (inputs.host-platform == 'linux-64' && 'linux-amd64-cpu8') || + (inputs.host-platform == 'linux-aarch64' && 'linux-arm64-cpu8') || + (inputs.host-platform == 'win-64' && 'windows-2022') }} + steps: + - name: Checkout ${{ github.event.repository.name }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.git-ref || github.ref }} + + - name: Capture checked-out commit SHA + run: echo "CHECKED_OUT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" + + - name: Set up MSVC + if: ${{ startsWith(inputs.host-platform, 'win') }} + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set environment variables + env: + CUDA_VER: ${{ inputs.cuda-version }} + HOST_PLATFORM: ${{ inputs.host-platform }} + PY_VER: ${{ matrix.python-version }} + SHA: ${{ env.CHECKED_OUT_SHA }} + run: ./ci/tools/env-vars build + + - name: Dump environment + run: env + + - name: Set up Python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Verify free-threaded build + if: ${{ endsWith(matrix.python-version, 't') }} + run: python -c 'import sys; assert not sys._is_gil_enabled()' + + - name: Set up Python include paths + run: | + if [[ "${{ inputs.host-platform }}" == linux* ]]; then + echo "CPLUS_INCLUDE_PATH=${Python3_ROOT_DIR}/include/python${{ matrix.python-version }}" >> "${GITHUB_ENV}" + elif [[ "${{ inputs.host-platform }}" == win* ]]; then + echo "CL=/I\"${Python3_ROOT_DIR}\include\python${{ matrix.python-version }}\"" >> "${GITHUB_ENV}" + fi + echo "PY_EXT_SUFFIX=$(python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")" >> "${GITHUB_ENV}" + + - name: Download cuda-pathfinder wheel + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: cuda-pathfinder-wheel + path: cuda_pathfinder + run-id: ${{ inputs.build-run-id }} + github-token: ${{ github.token }} + + - name: Download cuda.bindings wheels + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} + path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + run-id: ${{ inputs.build-run-id }} + github-token: ${{ github.token }} + + - name: Download cuda.core wheels + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} + path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} + run-id: ${{ inputs.build-run-id }} + github-token: ${{ github.token }} + + - name: Install cuda.pathfinder + run: pip install cuda_pathfinder/*.whl + + - name: Build cuda.bindings Cython tests + run: | + pip install "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/*.whl \ + --group ./cuda_bindings/pyproject.toml:test + pushd "${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}" + bash build_tests.sh + popd + + - name: Upload cuda.bindings Cython tests + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests + path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + if-no-files-found: error + + - name: Build cuda.core Cython tests + run: | + pip install "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/*.whl \ + --group ./cuda_core/pyproject.toml:test + pushd "${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}" + bash build_tests.sh + popd + + - name: Upload cuda.core Cython tests + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests + path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + if-no-files-found: error diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index adbf8a11be..bb809aa01f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -24,7 +24,7 @@ on: default: "" type: string run-id: - description: "The GHA run ID that generated validated artifacts" + description: "The workflow run ID that produced the artifacts to document" required: false default: ${{ github.run_id }} type: string @@ -108,91 +108,60 @@ jobs: echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${FILE_HASH}" >> $GITHUB_ENV echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "$REPO_DIR/cuda_bindings/dist")" >> $GITHUB_ENV + # Download all wheel artifacts from the given run-id. For a full CI run all + # four artifact groups exist; for a single-component release run only that + # component's artifact exists. pattern + merge-multiple produces an empty + # directory rather than failing when a named artifact is absent. - name: Download cuda-python build artifacts uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: cuda-python-wheel + pattern: cuda-python-wheel + merge-multiple: true path: . run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda-python artifacts - run: | - pwd - ls -lahR . - - name: Download cuda-pathfinder build artifacts uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: cuda-pathfinder-wheel + pattern: cuda-pathfinder-wheel + merge-multiple: true path: ./cuda_pathfinder run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda-pathfinder artifacts - run: | - pwd - ls -lahR cuda_pathfinder - - - name: Download cuda.bindings build artifacts - if: ${{ !inputs.is-release }} - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - - name: Download cuda.bindings build artifacts - if: ${{ inputs.is-release }} uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - pattern: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} + pattern: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-* merge-multiple: true path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda.bindings artifacts - run: | - pwd - ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR - - name: Download cuda.core build artifacts - if: ${{ !inputs.is-release }} uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - - name: Download cuda.core build artifacts - if: ${{ inputs.is-release }} - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - pattern: ${{ env.CUDA_CORE_ARTIFACT_NAME }} + pattern: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-* merge-multiple: true path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} run-id: ${{ inputs.run-id }} github-token: ${{ github.token }} - - name: Display structure of downloaded cuda.core build artifacts - run: | - pwd - ls -lahR $CUDA_CORE_ARTIFACTS_DIR + - name: Display downloaded artifacts + run: ls -lahR cuda_pathfinder "$CUDA_BINDINGS_ARTIFACTS_DIR" "$CUDA_CORE_ARTIFACTS_DIR" || true - - name: Install all packages + - name: Install documentation packages run: | - pushd cuda_pathfinder - pip install *.whl - popd - - pushd "${CUDA_BINDINGS_ARTIFACTS_DIR}" - pip install *.whl - popd - - pushd "${CUDA_CORE_ARTIFACTS_DIR}" - pip install *.whl - popd - - pip install cuda_python*.whl + shopt -s nullglob + pathfinder_wheels=(cuda_pathfinder/*.whl) + (( ${#pathfinder_wheels[@]} > 0 )) && pip install "${pathfinder_wheels[@]}" + bindings_wheels=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl) + (( ${#bindings_wheels[@]} > 0 )) && pip install "${bindings_wheels[@]}" + core_wheels=("${CUDA_CORE_ARTIFACTS_DIR}"/*.whl) + (( ${#core_wheels[@]} > 0 )) && pip install "${core_wheels[@]}" + python_wheels=(cuda_python*.whl) + (( ${#python_wheels[@]} > 0 )) && pip install "${python_wheels[@]}" # This step sets the PR_NUMBER/BUILD_LATEST/BUILD_PREVIEW env vars. - name: Get PR number @@ -206,7 +175,7 @@ jobs: mkdir -p artifacts/empty_docs - name: Build all docs - if: ${{ inputs.component == 'all' }} + if: ${{ inputs.component == '' || inputs.component == 'all' }} run: | pushd cuda_python/docs/ if [[ "${{ inputs.is-release }}" == "false" ]]; then @@ -221,7 +190,7 @@ jobs: mv cuda_python/docs/build/html/* artifacts/docs/ - name: Build component docs - if: ${{ inputs.component != 'all' }} + if: ${{ inputs.component != '' && inputs.component != 'all' }} run: | COMPONENT=$(echo "${{ inputs.component }}" | tr '-' '_') pushd ${COMPONENT}/docs/ diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 2a227d4ee9..d4a105f2c9 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -2,6 +2,12 @@ # # SPDX-License-Identifier: Apache-2.0 +# Reusable workflow: build wheels for one component on one platform. +# +# Callers choose the component; this workflow does exactly one thing per +# matrix row: checkout -> sccache -> (CTK if needed) -> cibuildwheel -> upload. +# All routing decisions live in the caller's job graph, not here. + on: workflow_call: inputs: @@ -11,16 +17,22 @@ on: cuda-version: required: true type: string - prev-cuda-version: + component: + description: "Which package to build: cuda-bindings | cuda-pathfinder" required: true type: string + git-ref: + description: "Git ref to check out (defaults to the caller's ref)" + required: false + default: "" + type: string defaults: run: shell: bash --noprofile --norc -xeuo pipefail {0} permissions: - contents: read # This is required for actions/checkout + contents: read jobs: build: @@ -43,6 +55,10 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + ref: ${{ inputs.git-ref || github.ref }} + + - name: Capture checked-out commit SHA + run: echo "CHECKED_OUT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" # The env vars ACTIONS_CACHE_SERVICE_V2, ACTIONS_RESULTS_URL, and ACTIONS_RUNTIME_TOKEN # are exposed by this action. @@ -50,7 +66,7 @@ jobs: uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # 0.0.9 # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - - name: Adding addtional GHA cache-related env vars + - name: Add additional GHA cache-related env vars uses: actions/github-script@v8 with: script: | @@ -60,16 +76,15 @@ jobs: - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main continue-on-error: true - # Skip cache on GitHub-hosted Windows runners. if: ${{ inputs.host-platform != 'win-64' }} with: enable-apt: true - name: Set up Python - id: setup-python1 + id: setup-python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - # WAR: setup-python is not relocatable, and cibuildwheel hard-wires to 3.12... + # WAR: setup-python is not relocatable, and cibuildwheel hard-wires to 3.12. # see https://github.com/actions/setup-python/issues/871 python-version: "3.12" @@ -77,12 +92,11 @@ jobs: if: ${{ startsWith(inputs.host-platform, 'win') }} uses: ilammy/msvc-dev-cmd@v1 # TODO: ask admin to allow pinning commits - - name: Set up yq + - name: Set up yq (Windows only) # GitHub made an unprofessional decision to not provide it in their Windows VMs, # see https://github.com/actions/runner-images/issues/7443. if: ${{ startsWith(inputs.host-platform, 'win') }} env: - # doesn't seem there's an easy way to avoid hard-coding it? YQ_URL: https://github.com/mikefarah/yq/releases/latest/download/yq_windows_amd64.exe YQ_DIR: yq_latest shell: pwsh -command ". '{0}'" @@ -99,64 +113,61 @@ jobs: CUDA_VER: ${{ inputs.cuda-version }} HOST_PLATFORM: ${{ inputs.host-platform }} PY_VER: ${{ matrix.python-version }} - SHA: ${{ github.sha }} - run: ./ci/tools/env-vars build + SHA: ${{ env.CHECKED_OUT_SHA }} + run: | + ./ci/tools/env-vars build + echo "SCCACHE_DIR=${HOME}/.cache/sccache" >> "${GITHUB_ENV}" + echo "SCCACHE_CACHE_SIZE=1G" >> "${GITHUB_ENV}" - name: Dump environment - run: | - env + run: env - name: Install twine - run: | - pip install twine + run: pip install twine - # To keep the build workflow simple, all matrix jobs will build a wheel for later use within this workflow. - - name: Build and check cuda.pathfinder wheel + # ----------------------------------------------------------------------- + # cuda-pathfinder: pure-Python, no CTK needed. + # We only need one wheel (pure-Python is platform-independent), so build + # it on linux-64, matrix index 0. + # ----------------------------------------------------------------------- + - name: Build cuda.pathfinder wheel + if: ${{ inputs.component == 'cuda-pathfinder' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} run: | pushd cuda_pathfinder pip wheel -v --no-deps . + twine check --strict *.whl + sudo chown -R "$(whoami)" *.whl + ls -lahR . popd - - name: List the cuda.pathfinder artifacts directory - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_pathfinder/*.whl - ls -lahR cuda_pathfinder - - # We only need/want a single pure python wheel, pick linux-64 index 0. - # This is what we will use for testing & releasing. - - name: Check cuda.pathfinder wheel - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - run: | - twine check --strict cuda_pathfinder/*.whl - - - name: Upload cuda.pathfinder build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Upload cuda.pathfinder wheel artifact + if: ${{ inputs.component == 'cuda-pathfinder' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: cuda-pathfinder-wheel path: cuda_pathfinder/*.whl if-no-files-found: error + # ----------------------------------------------------------------------- + # cuda-bindings: needs CTK, built with cibuildwheel across the full matrix. + # The linux-64 / first-Python row also builds the pure-Python cuda-python + # metapackage because it always ships with the same tag as cuda-bindings. + # ----------------------------------------------------------------------- - name: Set up mini CTK + if: ${{ inputs.component == 'cuda-bindings' }} uses: ./.github/actions/fetch_ctk - continue-on-error: false with: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ inputs.cuda-version }} - name: Build cuda.bindings wheel + if: ${{ inputs.component == 'cuda-bindings' }} uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: ./cuda_bindings/ output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} env: CIBW_BUILD: ${{ env.CIBW_BUILD }} - # CIBW mounts the host filesystem under /host CIBW_ENVIRONMENT_LINUX: > CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} @@ -173,290 +184,47 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > "/host/${{ env.SCCACHE_PATH }}" --show-stats - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. CIBW_TEST_COMMAND: > echo "ok!" - - name: List the cuda.bindings artifacts directory + - name: List and fix ownership of cuda.bindings artifacts + if: ${{ inputs.component == 'cuda-bindings' }} run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown + chown -R "$(whoami)" "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" else - export CHOWN="sudo chown" + sudo chown -R "$(whoami)" "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" fi - $CHOWN -R $(whoami) ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + ls -lahR "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" - name: Check cuda.bindings wheel - run: | - twine check --strict ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl + if: ${{ inputs.component == 'cuda-bindings' }} + run: twine check --strict "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}"/*.whl - - name: Upload cuda.bindings build artifacts + - name: Upload cuda.bindings wheel artifact + if: ${{ inputs.component == 'cuda-bindings' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl if-no-files-found: error - - name: Build cuda.core wheel - uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 - with: - package-dir: ./cuda_core/ - output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - env: - CIBW_BUILD: ${{ env.CIBW_BUILD }} - # CIBW mounts the host filesystem under /host - CIBW_ENVIRONMENT_LINUX: > - CUDA_PATH=/host/${{ env.CUDA_PATH }} - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} - PIP_FIND_LINKS=/host/${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - CC="/host/${{ env.SCCACHE_PATH }} cc" - CXX="/host/${{ env.SCCACHE_PATH }} c++" - SCCACHE_GHA_ENABLED=true - ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} - ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} - ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} - ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} - SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} - SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} - CIBW_ENVIRONMENT_WINDOWS: > - CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} - PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" - # check cache stats before leaving cibuildwheel - CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. - CIBW_TEST_COMMAND: > - echo "ok!" - - - name: List the cuda.core artifacts directory and rename - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" - for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do - if [[ -f "${wheel}" ]]; then - base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}/${new_name}" - echo "Renamed wheel to: ${new_name}" - fi - done - - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - # We only need/want a single pure python wheel, pick linux-64 index 0. - - name: Build and check cuda-python wheel - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Build cuda-python wheel + if: ${{ inputs.component == 'cuda-bindings' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} run: | pushd cuda_python pip wheel -v --no-deps . twine check --strict *.whl + sudo chown -R "$(whoami)" *.whl + ls -lahR . popd - - name: List the cuda-python artifacts directory - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_python/*.whl - ls -lahR cuda_python - - - name: Upload cuda-python build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Upload cuda-python wheel artifact + if: ${{ inputs.component == 'cuda-bindings' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: cuda-python-wheel path: cuda_python/*.whl if-no-files-found: error - - - name: Set up Python - id: setup-python2 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ matrix.python-version }} - - - name: verify free-threaded build - if: endsWith(matrix.python-version, 't') - run: python -c 'import sys; assert not sys._is_gil_enabled()' - - - name: Set up Python include paths - run: | - if [[ "${{ inputs.host-platform }}" == linux* ]]; then - echo "CPLUS_INCLUDE_PATH=${Python3_ROOT_DIR}/include/python${{ matrix.python-version }}" >> $GITHUB_ENV - elif [[ "${{ inputs.host-platform }}" == win* ]]; then - echo "CL=/I\"${Python3_ROOT_DIR}\include\python${{ matrix.python-version }}\"" >> $GITHUB_ENV - fi - # For caching - echo "PY_EXT_SUFFIX=$(python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")" >> $GITHUB_ENV - - - name: Install cuda.pathfinder (required for next step) - run: | - pip install cuda_pathfinder/*.whl - - - name: Build cuda.bindings Cython tests - run: | - pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl --group ./cuda_bindings/pyproject.toml:test - pushd ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} - bash build_tests.sh - popd - - - name: Upload cuda.bindings Cython tests - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - - - name: Build cuda.core Cython tests - run: | - pip install ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/"cu${BUILD_CUDA_MAJOR}"/*.whl --group ./cuda_core/pyproject.toml:test - pushd ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} - bash build_tests.sh - popd - - - name: Upload cuda.core Cython tests - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} - if-no-files-found: error - - # Note: This overwrites CUDA_PATH etc - - name: Set up mini CTK - uses: ./.github/actions/fetch_ctk - continue-on-error: false - with: - host-platform: ${{ inputs.host-platform }} - cuda-version: ${{ inputs.prev-cuda-version }} - cuda-path: "./cuda_toolkit_prev" - - - name: Download cuda.bindings build artifacts from the prior branch - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if ! (command -v gh 2>&1 >/dev/null); then - # See https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt. - # gh is needed for artifact fetching. - mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && apt update \ - && apt install gh -y - fi - - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) - OLD_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}*" - LATEST_PRIOR_RUN_ID=$(gh run list -b ${OLD_BRANCH} -L 1 -w "ci.yml" -s success -R NVIDIA/cuda-python --json databaseId | jq '.[]| .databaseId') - if [[ "$LATEST_PRIOR_RUN_ID" == "" ]]; then - echo "LATEST_PRIOR_RUN_ID not found!" - exit 1 - fi - - gh run download $LATEST_PRIOR_RUN_ID -p ${OLD_BASENAME} -R NVIDIA/cuda-python - rm -rf ${OLD_BASENAME}-tests # exclude cython test artifacts - ls -al $OLD_BASENAME - mkdir -p "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" - mv $OLD_BASENAME/*.whl "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" - rmdir $OLD_BASENAME - - - name: Build cuda.core wheel - uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 - with: - package-dir: ./cuda_core/ - output-dir: ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - env: - CIBW_BUILD: ${{ env.CIBW_BUILD }} - # CIBW mounts the host filesystem under /host - CIBW_ENVIRONMENT_LINUX: > - CUDA_PATH=/host/${{ env.CUDA_PATH }} - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} - PIP_FIND_LINKS=/host/${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - CC="/host/${{ env.SCCACHE_PATH }} cc" - CXX="/host/${{ env.SCCACHE_PATH }} c++" - SCCACHE_GHA_ENABLED=true - ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} - ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} - ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} - ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} - SCCACHE_DIR=/host/${{ env.SCCACHE_DIR }} - SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} - CIBW_ENVIRONMENT_WINDOWS: > - CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" - CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} - PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" - # check cache stats before leaving cibuildwheel - CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats - # force the test stage to be run (so that before-test is not skipped) - # TODO: we might want to think twice on adding this, it does a lot of - # things before reaching this command. - CIBW_TEST_COMMAND: > - echo "ok!" - - - name: List the cuda.core artifacts directory and rename - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}" - for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do - if [[ -f "${wheel}" ]]; then - base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_PREV_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}/${new_name}" - echo "Renamed wheel to: ${new_name}" - fi - done - - ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - - name: Merge cuda.core wheels - run: | - pip install wheel - python ci/tools/merge_cuda_core_wheels.py \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_CUDA_MAJOR}"/cuda_core*.whl \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_PREV_CUDA_MAJOR}"/cuda_core*.whl \ - --output-dir "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" - - - name: Check cuda.core wheel - run: | - twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl - - - name: Upload cuda.core build artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} - path: ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl - if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7e0c65224..00d2394811 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 -# Note: This name is referred to in the test job, so make sure any changes are sync'd up! -# Further this is referencing a run in the backport branch to fetch old bindings. +# Note: This name is referred to in build-cuda-core.yml when fetching backport-branch +# bindings artifacts, so keep it in sync with any renames. name: "CI" concurrency: @@ -15,12 +15,6 @@ on: branches: - "pull-request/[0-9]+" - "main" - tags: - # Build release artifacts from tag refs so setuptools-scm resolves exact - # release versions instead of .dev+local variants. - - "v*" - - "cuda-core-v*" - - "cuda-pathfinder-v*" schedule: # every 24 hours at midnight UTC - cron: "0 0 * * *" @@ -32,18 +26,16 @@ jobs: CUDA_BUILD_VER: ${{ steps.get-vars.outputs.cuda_build_ver }} CUDA_PREV_BUILD_VER: ${{ steps.get-vars.outputs.cuda_prev_build_ver }} steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Get CUDA build versions id: get-vars run: | cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) - echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT - + echo "cuda_build_ver=${cuda_build_ver}" >> "${GITHUB_OUTPUT}" cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml) - echo "cuda_prev_build_ver=$cuda_prev_build_ver" >> $GITHUB_OUTPUT + echo "cuda_prev_build_ver=${cuda_prev_build_ver}" >> "${GITHUB_OUTPUT}" should-skip: runs-on: ubuntu-latest @@ -51,8 +43,7 @@ jobs: skip: ${{ steps.get-should-skip.outputs.skip }} doc-only: ${{ steps.get-should-skip.outputs.doc_only }} steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Compute whether to skip builds and tests id: get-should-skip env: @@ -68,160 +59,142 @@ jobs: skip=false doc_only=false fi - echo "skip=${skip}" >> "$GITHUB_OUTPUT" - echo "doc_only=${doc_only}" >> "$GITHUB_OUTPUT" + echo "skip=${skip}" >> "${GITHUB_OUTPUT}" + echo "doc_only=${doc_only}" >> "${GITHUB_OUTPUT}" - # NOTE: Build jobs are intentionally split by platform rather than using a single - # matrix. This allows each test job to depend only on its corresponding build, - # so faster platforms can proceed through build & test without waiting for slower - # ones. Keep these job definitions textually identical except for: - # - host-platform value - # - if: condition (build-linux-64 omits doc-only check since it's needed for docs) - build-linux-64: - needs: - - ci-vars - - should-skip + # --------------------------------------------------------------------------- + # Wheel builds. + # --------------------------------------------------------------------------- + + # Pure-Python pathfinder is built on linux-64 only. The bindings build also + # emits the cuda-python metapackage on linux-64. + build-pathfinder: + needs: [ci-vars, should-skip] + if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} + secrets: inherit + uses: ./.github/workflows/build-wheel.yml + with: + host-platform: linux-64 + cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + component: cuda-pathfinder + + build-bindings: + needs: [ci-vars, should-skip] strategy: fail-fast: false matrix: - host-platform: - - linux-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + host-platform: ${{ fromJSON(fromJSON(needs.should-skip.outputs.doc-only) && '["linux-64"]' || '["linux-64","linux-aarch64","win-64"]') }} + name: Build ${{ matrix.host-platform }}, cuda.bindings, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} secrets: inherit uses: ./.github/workflows/build-wheel.yml with: host-platform: ${{ matrix.host-platform }} cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + component: cuda-bindings - # See build-linux-64 for why build jobs are split by platform. - build-linux-aarch64: - needs: - - ci-vars - - should-skip + # cuda.core needs the current-run bindings artifacts; its reusable workflow + # handles the current/previous CUDA split and final merge internally. + build-cuda-core: + needs: [ci-vars, should-skip, build-bindings] strategy: fail-fast: false matrix: - host-platform: - - linux-aarch64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} + host-platform: ${{ fromJSON(fromJSON(needs.should-skip.outputs.doc-only) && '["linux-64"]' || '["linux-64","linux-aarch64","win-64"]') }} + name: Build ${{ matrix.host-platform }}, cuda.core, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} secrets: inherit - uses: ./.github/workflows/build-wheel.yml + uses: ./.github/workflows/build-cuda-core.yml with: host-platform: ${{ matrix.host-platform }} cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + bindings-run-id: ${{ github.run_id }} - # See build-linux-64 for why build jobs are split by platform. - build-windows: - needs: - - ci-vars - - should-skip + # Cython test artifacts depend on the merged cuda.core wheels plus the + # bindings/pathfinder artifacts from this same CI run. + build-cython-tests: + needs: [ci-vars, should-skip, build-pathfinder, build-bindings, build-cuda-core] strategy: fail-fast: false matrix: host-platform: + - linux-64 + - linux-aarch64 - win-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }} + name: Build Cython tests ${{ matrix.host-platform }} + if: ${{ github.repository_owner == 'nvidia' && + !fromJSON(needs.should-skip.outputs.skip) && + !fromJSON(needs.should-skip.outputs.doc-only) }} secrets: inherit - uses: ./.github/workflows/build-wheel.yml + uses: ./.github/workflows/build-cython-tests.yml with: host-platform: ${{ matrix.host-platform }} cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + build-run-id: ${{ github.run_id }} + + # --------------------------------------------------------------------------- + # Tests consume artifacts from the current CI run after the build matrices + # finish. They remain split by platform because linux and windows use + # different reusable test workflows. + # --------------------------------------------------------------------------- - # NOTE: Test jobs are split by platform for the same reason as build jobs (see - # build-linux-64). Keep these job definitions textually identical except for: - # - host-platform value - # - build job under needs: - # - uses: (test-wheel-linux.yml vs test-wheel-windows.yml) test-linux-64: - strategy: - fail-fast: false - matrix: - host-platform: - - linux-64 - name: Test ${{ matrix.host-platform }} + needs: [ci-vars, should-skip, build-pathfinder, build-bindings, build-cuda-core, build-cython-tests] if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} permissions: - contents: read # This is required for actions/checkout - needs: - - ci-vars - - should-skip - - build-linux-64 + contents: read secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} + host-platform: linux-64 build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 100) || 1 }} - # See test-linux-64 for why test jobs are split by platform. test-linux-aarch64: - strategy: - fail-fast: false - matrix: - host-platform: - - linux-aarch64 - name: Test ${{ matrix.host-platform }} + needs: [ci-vars, should-skip, build-pathfinder, build-bindings, build-cuda-core, build-cython-tests] if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} permissions: - contents: read # This is required for actions/checkout - needs: - - ci-vars - - should-skip - - build-linux-aarch64 + contents: read secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} + host-platform: linux-aarch64 build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 100) || 1 }} - # See test-linux-64 for why test jobs are split by platform. test-windows: - strategy: - fail-fast: false - matrix: - host-platform: - - win-64 - name: Test ${{ matrix.host-platform }} + needs: [ci-vars, should-skip, build-pathfinder, build-bindings, build-cuda-core, build-cython-tests] if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }} permissions: - contents: read # This is required for actions/checkout - needs: - - ci-vars - - should-skip - - build-windows + contents: read secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: build-type: pull-request - host-platform: ${{ matrix.host-platform }} + host-platform: win-64 build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - nruns: ${{ (github.event_name == 'schedule' && 100) || 1}} + nruns: ${{ (github.event_name == 'schedule' && 100) || 1 }} + # --------------------------------------------------------------------------- + # Docs: needs linux-64 artifacts only. + # --------------------------------------------------------------------------- doc: name: Docs if: ${{ github.repository_owner == 'nvidia' }} - # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: id-token: write contents: write pull-requests: write - needs: - - ci-vars - - build-linux-64 + needs: [ci-vars, build-pathfinder, build-bindings, build-cuda-core] secrets: inherit uses: ./.github/workflows/build-docs.yml - with: - is-release: ${{ github.ref_type == 'tag' }} + # --------------------------------------------------------------------------- + # Final gate: checked by branch protection. + # --------------------------------------------------------------------------- checks: name: Check job status if: always() @@ -240,20 +213,6 @@ jobs: # see https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#always # and https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks # for why this cannot be encoded in the job-level `if:` field - # - # TL; DR: `$REASONS` - # - # The intersection of skipped-as-success and required status checks - # creates a scenario where if you DON'T `always()` run this job, the - # status check UI will block merging and if you DO `always()` run and - # a dependency is _cancelled_ (due to a critical failure, which is - # somehow not considered a failure ¯\_(ツ)_/¯) then the critically - # failing job(s) will timeout causing a cancellation here and the - # build to succeed which we don't want (originally this was just - # 'exit 0') - # - # Note: When [doc-only] is in PR title, test jobs are intentionally - # skipped and should not cause failure. doc_only=${{ needs.should-skip.outputs.doc-only }} if ${{ needs.doc.result == 'cancelled' || needs.doc.result == 'failure' }}; then exit 1 diff --git a/.github/workflows/release-cuda-pathfinder.yml b/.github/workflows/release-cuda-pathfinder.yml index c2ba575ecc..b3bed35d62 100644 --- a/.github/workflows/release-cuda-pathfinder.yml +++ b/.github/workflows/release-cuda-pathfinder.yml @@ -4,11 +4,12 @@ # One-click release workflow for cuda-pathfinder. # -# Provide a release tag. The workflow finds -# the CI run, creates a draft GitHub release with the standard -# body, builds versioned docs, uploads source archive + wheels to the +# Provide a release tag. The workflow verifies the tagged commit already passed +# CI on main, creates a draft GitHub release with the standard body, builds the +# release wheel in this workflow run, uploads source archive + wheels to the # release, publishes to TestPyPI, verifies the install, publishes to PyPI, # verifies again, and finally marks the release as published. +# Docs are built and deployed separately via build-docs.yml. name: "Release: cuda-pathfinder" @@ -30,7 +31,7 @@ defaults: jobs: # -------------------------------------------------------------------------- - # Collect release metadata, find the CI run, create a draft release. + # Collect release metadata, verify the tagged main commit, create a draft release. # -------------------------------------------------------------------------- prepare: runs-on: ubuntu-latest @@ -39,7 +40,8 @@ jobs: outputs: tag: ${{ steps.vars.outputs.tag }} version: ${{ steps.vars.outputs.version }} - run-id: ${{ steps.detect-run.outputs.run-id }} + run-id: ${{ steps.vars.outputs.run-id }} + cuda-build-ver: ${{ steps.cuda.outputs.cuda-build-ver }} steps: - name: Verify running on default branch run: | @@ -52,11 +54,13 @@ jobs: id: vars env: TAG_INPUT: ${{ inputs.tag }} + RUN_ID: ${{ github.run_id }} run: | version="${TAG_INPUT#cuda-pathfinder-v}" { echo "tag=${TAG_INPUT}" echo "version=${version}" + echo "run-id=${RUN_ID}" } >> "$GITHUB_OUTPUT" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -76,14 +80,17 @@ jobs: exit 1 fi - - name: Detect CI run ID - id: detect-run + - name: Verify release tag preconditions env: GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.vars.outputs.tag }} run: | - run_id=$(./ci/tools/lookup-run-id "${TAG}" "${{ github.repository }}") - echo "run-id=${run_id}" >> "$GITHUB_OUTPUT" + ./ci/tools/verify-release-commit "${{ steps.vars.outputs.tag }}" "${{ github.repository }}" "${{ github.event.repository.default_branch }}" + + - name: Read CUDA build versions + id: cuda + run: | + cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) + echo "cuda-build-ver=${cuda_build_ver}" >> "$GITHUB_OUTPUT" - name: Create draft release env: @@ -127,75 +134,38 @@ jobs: --notes-file /tmp/release-body.md # -------------------------------------------------------------------------- - # Build and deploy versioned docs. + # Build the release wheel in this workflow run. # -------------------------------------------------------------------------- - docs: + build-release: needs: prepare - if: ${{ github.repository_owner == 'nvidia' }} - permissions: - id-token: write - contents: write - pull-requests: write secrets: inherit - uses: ./.github/workflows/build-docs.yml + uses: ./.github/workflows/build-wheel.yml with: + host-platform: linux-64 + cuda-version: ${{ needs.prepare.outputs.cuda-build-ver }} + git-ref: refs/tags/${{ needs.prepare.outputs.tag }} component: cuda-pathfinder - git-tag: ${{ needs.prepare.outputs.tag }} - run-id: ${{ needs.prepare.outputs.run-id }} - is-release: true # -------------------------------------------------------------------------- # Upload source archive and wheels to the GitHub release. - # Runs even if docs fail -- assets are independent and the finalize - # job's docs-URL check will warn if docs aren't deployed yet. # -------------------------------------------------------------------------- upload-assets: - needs: [prepare, docs] + needs: [prepare, build-release] if: ${{ !cancelled() && needs.prepare.result == 'success' }} - runs-on: ubuntu-latest permissions: contents: write - env: - TAG: ${{ needs.prepare.outputs.tag }} - RUN_ID: ${{ needs.prepare.outputs.run-id }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - ref: ${{ needs.prepare.outputs.tag }} - - - name: Create source archive - run: | - archive="${{ github.event.repository.name }}-${TAG}" - mkdir -p release - git archive \ - --format=tar.gz \ - --prefix="${archive}/" \ - --output="release/${archive}.tar.gz" \ - "${TAG}" - sha256sum "release/${archive}.tar.gz" \ - | awk '{print $1}' > "release/${archive}.tar.gz.sha256sum" - - - name: Download wheels - env: - GH_TOKEN: ${{ github.token }} - run: | - ./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "release/wheels" - - - name: Upload to release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release upload "${TAG}" \ - --repo "${{ github.repository }}" \ - --clobber \ - release/*.tar.gz release/*.sha256sum release/wheels/*.whl + secrets: inherit + uses: ./.github/workflows/release-upload.yml + with: + git-tag: ${{ needs.prepare.outputs.tag }} + run-id: ${{ needs.prepare.outputs.run-id }} + component: cuda-pathfinder # -------------------------------------------------------------------------- # Publish to TestPyPI. # -------------------------------------------------------------------------- publish-testpypi: - needs: [prepare, docs] + needs: [prepare, build-release] if: ${{ !cancelled() && needs.prepare.result == 'success' }} runs-on: ubuntu-latest environment: @@ -203,32 +173,22 @@ jobs: url: https://test.pypi.org/p/cuda-pathfinder/ permissions: id-token: write - env: - RUN_ID: ${{ needs.prepare.outputs.run-id }} steps: - - name: Download wheels + - name: Checkout source at release tag + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ needs.prepare.outputs.tag }} + + - name: Download wheels from this release run env: GH_TOKEN: ${{ github.token }} run: | - # Intentionally inline artifact download logic here so publish jobs - # are pinned only to RUN_ID artifacts and do not depend on repo checkout. - mkdir -p dist - gh run download "${RUN_ID}" -p "cuda-pathfinder*" -R "${{ github.repository }}" - shopt -s nullglob globstar - for artifact_dir in cuda-*; do - if [[ ! -d "${artifact_dir}" ]]; then - continue - fi - if [[ "${artifact_dir}" == *-tests ]]; then - continue - fi - wheels=( "${artifact_dir}"/**/*.whl ) - if (( ${#wheels[@]} > 0 )); then - mv "${wheels[@]}" dist/ - fi - done - rm -rf cuda-* - ls -la dist + ./ci/tools/download-wheels "${{ needs.prepare.outputs.run-id }}" "cuda-pathfinder" "${{ github.repository }}" "dist" + + - name: Validate wheel versions for release tag + run: | + ./ci/tools/validate-release-wheels "${{ needs.prepare.outputs.tag }}" "cuda-pathfinder" "dist" - name: Publish to TestPyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 @@ -280,32 +240,22 @@ jobs: url: https://pypi.org/p/cuda-pathfinder/ permissions: id-token: write - env: - RUN_ID: ${{ needs.prepare.outputs.run-id }} steps: - - name: Download wheels + - name: Checkout source at release tag + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ needs.prepare.outputs.tag }} + + - name: Download wheels from this release run env: GH_TOKEN: ${{ github.token }} run: | - # Intentionally inline artifact download logic here so publish jobs - # are pinned only to RUN_ID artifacts and do not depend on repo checkout. - mkdir -p dist - gh run download "${RUN_ID}" -p "cuda-pathfinder*" -R "${{ github.repository }}" - shopt -s nullglob globstar - for artifact_dir in cuda-*; do - if [[ ! -d "${artifact_dir}" ]]; then - continue - fi - if [[ "${artifact_dir}" == *-tests ]]; then - continue - fi - wheels=( "${artifact_dir}"/**/*.whl ) - if (( ${#wheels[@]} > 0 )); then - mv "${wheels[@]}" dist/ - fi - done - rm -rf cuda-* - ls -la dist + ./ci/tools/download-wheels "${{ needs.prepare.outputs.run-id }}" "cuda-pathfinder" "${{ github.repository }}" "dist" + + - name: Validate wheel versions for release tag + run: | + ./ci/tools/validate-release-wheels "${{ needs.prepare.outputs.tag }}" "cuda-pathfinder" "dist" - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/release-upload.yml b/.github/workflows/release-upload.yml index 52a34e6c77..4575d93144 100644 --- a/.github/workflows/release-upload.yml +++ b/.github/workflows/release-upload.yml @@ -11,7 +11,7 @@ on: type: string required: true run-id: - description: "The GHA run ID that generated validated artifacts" + description: "The workflow run ID that built the release artifacts" type: string required: true component: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97d58d8ae5..a4b983e675 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,12 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 name: "CI: Release" -# Manually-triggered release workflow. Creates a release draft if one doesn't exist -# for the given tag, or uses an existing draft, then publishes the selected wheels -# to TestPyPI followed by PyPI. +# Manually-triggered release workflow. Builds release artifacts from the tag in +# this workflow run after verifying the tagged commit already passed main CI, +# then publishes the selected wheels to TestPyPI followed by PyPI. on: workflow_dispatch: @@ -18,132 +18,196 @@ on: options: - cuda-core - cuda-bindings - - cuda-pathfinder - - cuda-python - - all git-tag: description: "The release git tag" required: true type: string - run-id: - description: "The GHA run ID that generated validated artifacts (optional - auto-detects successful tag-triggered CI run for git-tag)" - required: false - type: string - default: "" defaults: run: shell: bash --noprofile --norc -xeuo pipefail {0} jobs: - determine-run-id: + prepare: runs-on: ubuntu-latest + permissions: + contents: write outputs: - run-id: ${{ steps.lookup-run-id.outputs.run-id }} + target-component: ${{ steps.plan.outputs.target-component }} + run-id: ${{ steps.plan.outputs.run-id }} + cuda-build-ver: ${{ steps.cuda.outputs.cuda-build-ver }} + cuda-prev-build-ver: ${{ steps.cuda.outputs.cuda-prev-build-ver }} steps: - - name: Checkout Source + - name: Verify running on default branch + run: | + if [[ "${{ github.ref_name }}" != "${{ github.event.repository.default_branch }}" ]]; then + echo "::error::This workflow must be triggered from the default branch (${{ github.event.repository.default_branch }}). Got: ${{ github.ref_name }}." + exit 1 + fi + + - name: Validate component and tag combination + id: plan + env: + COMPONENT_INPUT: ${{ inputs.component }} + TAG_INPUT: ${{ inputs.git-tag }} + RUN_ID: ${{ github.run_id }} + run: | + case "${TAG_INPUT}" in + v*) + if [[ "${COMPONENT_INPUT}" != "cuda-bindings" ]]; then + echo "::error::Tag ${TAG_INPUT} only supports cuda-bindings releases (cuda-python is built alongside cuda-bindings)." + exit 1 + fi + ;; + cuda-core-v*) + if [[ "${COMPONENT_INPUT}" != "cuda-core" ]]; then + echo "::error::Tag ${TAG_INPUT} only supports cuda-core releases." + exit 1 + fi + ;; + *) + echo "::error::Unsupported tag ${TAG_INPUT}. Use the dedicated cuda-pathfinder release workflow for cuda-pathfinder tags." + exit 1 + ;; + esac + + { + echo "target-component=${COMPONENT_INPUT}" + echo "run-id=${RUN_ID}" + } >> "${GITHUB_OUTPUT}" + + - name: Checkout source at release tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - # fetch-depth: 0 is required so the lookup-run-id script can access all git tags fetch-depth: 0 + ref: refs/tags/${{ inputs.git-tag }} - - name: Determine Run ID - id: lookup-run-id + - name: Verify release tag preconditions env: GH_TOKEN: ${{ github.token }} run: | - echo "Auto-detecting successful tag-triggered run ID for tag: ${{ inputs.git-tag }}" - RUN_ID=$(./ci/tools/lookup-run-id "${{ inputs.git-tag }}" "${{ github.repository }}") - echo "Auto-detected run ID: $RUN_ID" - echo "run-id=$RUN_ID" >> "$GITHUB_OUTPUT" + ./ci/tools/verify-release-commit "${{ inputs.git-tag }}" "${{ github.repository }}" "${{ github.event.repository.default_branch }}" - check-tag: - runs-on: ubuntu-latest - steps: - - name: Checkout Source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 + - name: Read CUDA build versions + id: cuda + run: | + cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) + cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml) + { + echo "cuda-build-ver=${cuda_build_ver}" + echo "cuda-prev-build-ver=${cuda_prev_build_ver}" + } >> "${GITHUB_OUTPUT}" - name: Check or create draft release for the tag env: GH_TOKEN: ${{ github.token }} run: | - mapfile -t tags < <(gh release list -R "${{ github.repository }}" --json tagName --jq '.[] | .tagName') - mapfile -t is_draft < <(gh release list -R "${{ github.repository }}" --json isDraft --jq '.[] | .isDraft') - - found=0 - for idx in "${!tags[@]}"; do - if [[ "${tags[$idx]}" == "${{ inputs.git-tag }}" ]]; then - echo "found existing release for ${{ inputs.git-tag }}" - found=1 - if [[ "${is_draft[$idx]}" != "true" ]]; then - echo "the release note is not in draft state" - exit 1 - fi - break - fi - done - if [[ "$found" == 0 ]]; then - echo "no release found for ${{ inputs.git-tag }}, creating draft release" - gh release create "${{ inputs.git-tag }}" --draft --repo "${{ github.repository }}" --title "Release ${{ inputs.git-tag }}" --notes "Release ${{ inputs.git-tag }}" + existing_draft=$(gh release view "${{ inputs.git-tag }}" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null || echo "missing") + if [[ "${existing_draft}" == "false" ]]; then + echo "::error::Release ${{ inputs.git-tag }} already exists and is published." + exit 1 fi - - doc: - name: Build release docs - if: ${{ github.repository_owner == 'nvidia' }} - # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages - permissions: - id-token: write - contents: write - pull-requests: write - needs: - - check-tag - - determine-run-id + if [[ "${existing_draft}" == "true" ]]; then + exit 0 + fi + gh release create "${{ inputs.git-tag }}" \ + --draft \ + --repo "${{ github.repository }}" \ + --title "Release ${{ inputs.git-tag }}" \ + --notes "Release ${{ inputs.git-tag }}" + + # cuda-bindings releases publish bindings wheels on all platforms; the + # reusable workflow also emits the cuda-python metapackage on linux-64. + # cuda-core releases still need current-run bindings artifacts as an internal + # dependency, so build-bindings runs for both components. + build-bindings: + needs: prepare + if: ${{ inputs.component == 'cuda-bindings' || inputs.component == 'cuda-core' }} + strategy: + fail-fast: false + matrix: + host-platform: + - linux-64 + - linux-aarch64 + - win-64 + name: Build ${{ matrix.host-platform }}, cuda.bindings secrets: inherit - uses: ./.github/workflows/build-docs.yml + uses: ./.github/workflows/build-wheel.yml with: - component: ${{ inputs.component }} - git-tag: ${{ inputs.git-tag }} - run-id: ${{ needs.determine-run-id.outputs.run-id }} - is-release: true + host-platform: ${{ matrix.host-platform }} + cuda-version: ${{ needs.prepare.outputs.cuda-build-ver }} + component: cuda-bindings + git-ref: refs/tags/${{ inputs.git-tag }} + + build-cuda-core: + needs: [prepare, build-bindings] + if: ${{ inputs.component == 'cuda-core' }} + strategy: + fail-fast: false + matrix: + host-platform: + - linux-64 + - linux-aarch64 + - win-64 + name: Build ${{ matrix.host-platform }}, cuda.core + secrets: inherit + uses: ./.github/workflows/build-cuda-core.yml + with: + host-platform: ${{ matrix.host-platform }} + cuda-version: ${{ needs.prepare.outputs.cuda-build-ver }} + prev-cuda-version: ${{ needs.prepare.outputs.cuda-prev-build-ver }} + bindings-run-id: ${{ github.run_id }} + git-ref: refs/tags/${{ inputs.git-tag }} upload-archive: name: Upload source archive permissions: contents: write needs: - - check-tag - - determine-run-id - - doc + - prepare + - build-bindings + - build-cuda-core + if: ${{ always() && + !cancelled() && + needs.prepare.result == 'success' && + needs.build-bindings.result == 'success' && + (inputs.component == 'cuda-bindings' || needs.build-cuda-core.result == 'success') }} secrets: inherit uses: ./.github/workflows/release-upload.yml with: git-tag: ${{ inputs.git-tag }} - run-id: ${{ needs.determine-run-id.outputs.run-id }} + run-id: ${{ needs.prepare.outputs.run-id }} component: ${{ inputs.component }} publish-testpypi: name: Publish wheels to TestPyPI runs-on: ubuntu-latest needs: - - check-tag - - determine-run-id - - doc + - prepare + - build-bindings + - build-cuda-core + if: ${{ always() && + !cancelled() && + needs.prepare.result == 'success' && + needs.build-bindings.result == 'success' && + (inputs.component == 'cuda-bindings' || needs.build-cuda-core.result == 'success') }} environment: name: testpypi - url: https://test.pypi.org/${{ inputs.component != 'all' && format('p/{0}/', inputs.component) || '' }} + url: https://test.pypi.org/p/${{ inputs.component }}/ permissions: id-token: write steps: - - name: Checkout Source + - name: Checkout source at release tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: refs/tags/${{ inputs.git-tag }} - - name: Download component wheels + - name: Download component wheels from this release run env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ./ci/tools/download-wheels "${{ needs.determine-run-id.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" + ./ci/tools/download-wheels "${{ needs.prepare.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" - name: Validate wheel versions for release tag run: | @@ -158,22 +222,25 @@ jobs: name: Publish wheels to PyPI runs-on: ubuntu-latest needs: - - determine-run-id + - prepare - publish-testpypi environment: name: pypi - url: https://pypi.org/${{ inputs.component != 'all' && format('p/{0}/', inputs.component) || '' }} + url: https://pypi.org/p/${{ inputs.component }}/ permissions: id-token: write steps: - - name: Checkout Source + - name: Checkout source at release tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: refs/tags/${{ inputs.git-tag }} - - name: Download component wheels + - name: Download component wheels from this release run env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ./ci/tools/download-wheels "${{ needs.determine-run-id.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" + ./ci/tools/download-wheels "${{ needs.prepare.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" - name: Validate wheel versions for release tag run: | @@ -181,5 +248,3 @@ jobs: - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - - # TODO: add another job to make the release leave the draft state? diff --git a/ci/tools/download-wheels b/ci/tools/download-wheels index a3141afb33..160f471684 100755 --- a/ci/tools/download-wheels +++ b/ci/tools/download-wheels @@ -32,13 +32,23 @@ fi echo "Downloading wheels for component: $COMPONENT from run: $RUN_ID" -# Download component wheels using the same logic as release.yml -if [[ "$COMPONENT" == "all" ]]; then - # Download all component patterns - gh run download "$RUN_ID" -p "cuda-*" -R "$REPOSITORY" -else - gh run download "$RUN_ID" -p "${COMPONENT}*" -R "$REPOSITORY" -fi +# Build the list of artifact name patterns to download. +# cuda-bindings always ships alongside cuda-python (same tag, same run). +case "$COMPONENT" in + cuda-bindings) + PATTERNS=("cuda-bindings*" "cuda-python-wheel") + ;; + all) + PATTERNS=("cuda-*") + ;; + *) + PATTERNS=("${COMPONENT}*") + ;; +esac + +for pattern in "${PATTERNS[@]}"; do + gh run download "$RUN_ID" -p "$pattern" -R "$REPOSITORY" || true +done # Create output directory mkdir -p "$OUTPUT_DIR" @@ -56,16 +66,8 @@ do continue fi - # If we're not downloading "all", only process matching component - if [[ "$COMPONENT" != "all" && "$p" != ${COMPONENT}* ]]; then - continue - fi - echo "Processing artifact: $p" - # Move wheel files to output directory - if [[ -d "$p" ]]; then - find "$p" -name "*.whl" -exec mv {} "$OUTPUT_DIR/" \; - fi + find "$p" -name "*.whl" -exec mv {} "$OUTPUT_DIR/" \; done # Clean up artifact directories diff --git a/ci/tools/lookup-run-id b/ci/tools/lookup-run-id deleted file mode 100755 index 092f8369ae..0000000000 --- a/ci/tools/lookup-run-id +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -# A utility script to find the GitHub Actions workflow run ID for a given git tag. -# This script requires a successful CI run that was triggered by the tag push. - -set -euo pipefail - -# Check required arguments -if [[ $# -lt 2 ]]; then - echo "Usage: $0 [workflow-name]" >&2 - echo " git-tag: The git tag to find the corresponding workflow run for" >&2 - echo " repository: The GitHub repository (e.g., NVIDIA/cuda-python)" >&2 - echo " workflow-name: Optional workflow name to filter by (default: CI)" >&2 - echo "" >&2 - echo "Examples:" >&2 - echo " $0 v13.0.1 NVIDIA/cuda-python" >&2 - echo " $0 v13.0.1 NVIDIA/cuda-python \"CI\"" >&2 - exit 1 -fi - -GIT_TAG="${1}" -REPOSITORY="${2}" -WORKFLOW_NAME="${3:-CI}" - -# Ensure we have required tools -if [[ -z "${GH_TOKEN:-}" ]]; then - echo "Error: GH_TOKEN environment variable is required" >&2 - exit 1 -fi - -if ! command -v jq >/dev/null 2>&1; then - echo "Error: jq is required but not installed" >&2 - exit 1 -fi - -if ! command -v gh >/dev/null 2>&1; then - echo "Error: GitHub CLI (gh) is required but not installed" >&2 - exit 1 -fi - -echo "Looking up run ID for tag: ${GIT_TAG} in repository: ${REPOSITORY}" >&2 - -# Resolve git tag to commit SHA -if ! COMMIT_SHA=$(git rev-parse "${GIT_TAG}"); then - echo "Error: Could not resolve git tag '${GIT_TAG}' to a commit SHA" >&2 - echo "Make sure the tag exists and you have fetched it" >&2 - exit 1 -fi - -echo "Resolved tag '${GIT_TAG}' to commit: ${COMMIT_SHA}" >&2 - -# Find workflow runs for this commit -echo "Searching for '${WORKFLOW_NAME}' workflow runs for commit: ${COMMIT_SHA} (tag: ${GIT_TAG})" >&2 - -# Get completed workflow runs for this commit. -RUN_DATA=$(gh run list \ - --repo "${REPOSITORY}" \ - --commit "${COMMIT_SHA}" \ - --workflow "${WORKFLOW_NAME}" \ - --status completed \ - --json databaseId,workflowName,status,conclusion,headSha,headBranch,event,createdAt,url \ - --limit 50) - -if [[ -z "${RUN_DATA}" || "${RUN_DATA}" == "[]" ]]; then - echo "Error: No completed '${WORKFLOW_NAME}' workflow runs found for commit ${COMMIT_SHA}" >&2 - echo "Available workflow runs for this commit:" >&2 - gh run list --repo "${REPOSITORY}" --commit "${COMMIT_SHA}" --limit 10 || true - exit 1 -fi - -# Filter for successful push runs from the tag ref. -RUN_ID=$(echo "${RUN_DATA}" | jq -r --arg tag "${GIT_TAG}" ' - map(select(.conclusion == "success" and .event == "push" and .headBranch == $tag)) - | sort_by(.createdAt) - | reverse - | .[0].databaseId // empty -') - -if [[ -z "${RUN_ID}" ]]; then - echo "Error: No successful '${WORKFLOW_NAME}' workflow runs found for tag '${GIT_TAG}'." >&2 - echo "This release workflow now requires artifacts from a tag-triggered CI run." >&2 - echo "If you just pushed the tag, wait for CI on that tag to finish and retry." >&2 - echo "" >&2 - echo "Completed runs for commit ${COMMIT_SHA}:" >&2 - echo "${RUN_DATA}" | jq -r '.[] | "\(.databaseId): event=\(.event // "null"), headBranch=\(.headBranch // "null"), conclusion=\(.conclusion // "null"), status=\(.status // "null"), createdAt=\(.createdAt // "null")"' >&2 - exit 1 -fi - -echo "Found workflow run ID: ${RUN_ID} for tag '${GIT_TAG}'" >&2 - -# Verify the run has the expected artifacts by checking if there are any artifacts -echo "Verifying artifacts exist for run ${RUN_ID}..." >&2 -ARTIFACT_LIST=$(gh run view "${RUN_ID}" --repo "${REPOSITORY}" --json url || echo "") - -if [[ -z "${ARTIFACT_LIST}" ]]; then - echo "Warning: Could not verify artifacts for workflow run ${RUN_ID}" >&2 -fi - -# Output the run ID (this is what gets used by calling scripts) -echo "${RUN_ID}" diff --git a/ci/tools/validate-release-wheels b/ci/tools/validate-release-wheels index 5757ca17bc..7f7a7156df 100755 --- a/ci/tools/validate-release-wheels +++ b/ci/tools/validate-release-wheels @@ -16,9 +16,9 @@ from pathlib import Path COMPONENT_TO_DISTRIBUTIONS: dict[str, set[str]] = { "cuda-core": {"cuda_core"}, - "cuda-bindings": {"cuda_bindings"}, + # cuda-python is a pure-Python metapackage always released with cuda-bindings. + "cuda-bindings": {"cuda_bindings", "cuda_python"}, "cuda-pathfinder": {"cuda_pathfinder"}, - "cuda-python": {"cuda_python"}, "all": {"cuda_core", "cuda_bindings", "cuda_pathfinder", "cuda_python"}, } @@ -37,7 +37,11 @@ def parse_args() -> argparse.Namespace: ) ) parser.add_argument("git_tag", help="Release git tag (for example: v13.0.0)") - parser.add_argument("component", choices=sorted(COMPONENT_TO_DISTRIBUTIONS.keys())) + parser.add_argument( + "component", + choices=sorted(COMPONENT_TO_DISTRIBUTIONS.keys()), + help="Component being released; 'cuda-bindings' implicitly includes cuda-python.", + ) parser.add_argument("wheel_dir", help="Directory containing wheel files") return parser.parse_args() diff --git a/ci/tools/verify-release-commit b/ci/tools/verify-release-commit new file mode 100755 index 0000000000..e2a875e8d0 --- /dev/null +++ b/ci/tools/verify-release-commit @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Verify that a release tag points to a commit already validated by main CI. + +set -euo pipefail + +if [[ $# -lt 3 ]]; then + echo "Usage: $0 [workflow-name]" >&2 + echo " git-tag: The release git tag to verify" >&2 + echo " repository: The GitHub repository (e.g. NVIDIA/cuda-python)" >&2 + echo " default-branch: The branch releases must come from (e.g. main)" >&2 + echo " workflow-name: Optional workflow name to check (default: CI)" >&2 + exit 1 +fi + +GIT_TAG="${1}" +REPOSITORY="${2}" +DEFAULT_BRANCH="${3}" +WORKFLOW_NAME="${4:-CI}" + +if [[ -z "${GH_TOKEN:-}" ]]; then + echo "Error: GH_TOKEN environment variable is required" >&2 + exit 1 +fi + +for exe in gh jq git; do + if ! command -v "${exe}" >/dev/null 2>&1; then + echo "Error: ${exe} is required but not installed" >&2 + exit 1 + fi +done + +if ! COMMIT_SHA=$(git rev-parse "${GIT_TAG}^{commit}"); then + echo "Error: Could not resolve git tag '${GIT_TAG}' to a commit SHA" >&2 + exit 1 +fi + +echo "Resolved tag '${GIT_TAG}' to commit ${COMMIT_SHA}" >&2 + +git fetch origin "${DEFAULT_BRANCH}" --depth=1 >/dev/null 2>&1 + +if ! git merge-base --is-ancestor "${COMMIT_SHA}" "origin/${DEFAULT_BRANCH}"; then + echo "Error: Tag '${GIT_TAG}' does not point at a commit reachable from origin/${DEFAULT_BRANCH}" >&2 + exit 1 +fi + +RUN_DATA=$(gh run list \ + --repo "${REPOSITORY}" \ + --branch "${DEFAULT_BRANCH}" \ + --commit "${COMMIT_SHA}" \ + --workflow "${WORKFLOW_NAME}" \ + --json databaseId,conclusion,status,event,headBranch,createdAt,url \ + --limit 20) + +RUN_ID=$(echo "${RUN_DATA}" | jq -r --arg branch "${DEFAULT_BRANCH}" ' + map(select(.conclusion == "success" and .event == "push" and .headBranch == $branch)) + | sort_by(.createdAt) + | reverse + | .[0].databaseId // empty +') + +if [[ -z "${RUN_ID}" ]]; then + echo "Error: No successful '${WORKFLOW_NAME}' run found on ${DEFAULT_BRANCH} for commit ${COMMIT_SHA}" >&2 + echo "Release tags must point at commits already validated on ${DEFAULT_BRANCH}." >&2 + echo "" >&2 + echo "Candidate runs for that commit:" >&2 + echo "${RUN_DATA}" | jq -r '.[] | "\(.databaseId): branch=\(.headBranch // "null"), event=\(.event // "null"), conclusion=\(.conclusion // "null"), status=\(.status // "null"), createdAt=\(.createdAt // "null")"' >&2 + exit 1 +fi + +echo "Verified ${DEFAULT_BRANCH} CI run ${RUN_ID} for commit ${COMMIT_SHA}" >&2 +echo "${COMMIT_SHA}"