Repository navigation
CI: Release #244
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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. The dry-run mode validates the release path | |
| # without publishing to external release surfaces. | |
| # Pathfinder additionally prepares metadata, verifies both indexes, and publishes its draft. | |
| # | |
| # Maintenance note: non-trivial changes to this workflow should be validated | |
| # with dry-run workflow_dispatch runs before merging. Suggested focused matrix: | |
| # - mainline: | |
| # component=cuda-bindings | |
| # git-tag=<latest cuda-bindings release tag> | |
| # backport-git-tag=not planned | |
| # run-id=<blank> | |
| # dry-run-docs-branch=gh-pages-dry-run | |
| # - backport sequence: | |
| # 1. component=cuda-bindings | |
| # git-tag=<latest cuda-bindings backport release tag> | |
| # backport-git-tag=<blank> | |
| # 2. component=cuda-python | |
| # git-tag=<matching cuda-python backport release tag> | |
| # backport-git-tag=<blank> | |
| # run-id=<blank> | |
| # dry-run-docs-branch=gh-pages-dry-run | |
| # Leave run-id blank so determine-run-id is exercised. For exhaustive coverage, | |
| # add a mainline cuda-python dry-run when changes could affect metapackage | |
| # artifact validation, docs routing, or component-specific release behavior. | |
| # Also exercise cuda-core and cuda-pathfinder, including a failed docs build. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| component: | |
| description: "Component to release" | |
| required: true | |
| type: choice | |
| options: | |
| - cuda-core | |
| - cuda-bindings | |
| - cuda-pathfinder | |
| - cuda-python | |
| release-action: | |
| description: "What to run" | |
| required: true | |
| type: choice | |
| options: | |
| - dry-run | |
| - full-release | |
| default: dry-run | |
| git-tag: | |
| description: "The release git tag" | |
| required: true | |
| type: string | |
| backport-git-tag: | |
| description: "Mainline cuda-bindings/cuda-python only: planned backport tag, or 'not planned'. Leave blank for backport releases." | |
| required: false | |
| type: string | |
| default: "" | |
| 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: "" | |
| dry-run-docs-branch: | |
| description: "Dry-run only: optional gh-pages-* branch to receive generated docs, for example gh-pages-dry-run" | |
| required: false | |
| type: string | |
| default: "" | |
| concurrency: | |
| # Serialize releases per component without canceling an active run. | |
| group: release-${{ inputs.component }} | |
| cancel-in-progress: false | |
| defaults: | |
| run: | |
| shell: bash --noprofile --norc -xeuo pipefail {0} | |
| jobs: | |
| determine-run-id: | |
| if: ${{ github.repository_owner == 'nvidia' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run-id: ${{ steps.lookup-run-id.outputs.run-id }} | |
| steps: | |
| - name: Checkout Source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # lookup-run-id resolves the git tag to a SHA; we need tags but not history. | |
| fetch-depth: 1 | |
| fetch-tags: true | |
| - name: Determine Run ID | |
| id: lookup-run-id | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_GIT_TAG: ${{ inputs.git-tag }} | |
| PROVIDED_RUN_ID: ${{ inputs.run-id }} | |
| RELEASE_REPOSITORY: ${{ github.repository }} | |
| run: | | |
| if [[ -n "$PROVIDED_RUN_ID" ]]; then | |
| echo "Using provided run ID: $PROVIDED_RUN_ID" | |
| RUN_ID="$PROVIDED_RUN_ID" | |
| else | |
| echo "Auto-detecting successful tag-triggered run ID for tag: $RELEASE_GIT_TAG" | |
| RUN_ID=$(./ci/tools/lookup-run-id "$RELEASE_GIT_TAG" "$RELEASE_REPOSITORY") | |
| echo "Auto-detected run ID: $RUN_ID" | |
| fi | |
| echo "run-id=$RUN_ID" >> "$GITHUB_OUTPUT" | |
| check-tag: | |
| if: ${{ github.repository_owner == 'nvidia' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # Dry-run validation resolves the requested tag locally; we need tags but not history. | |
| fetch-depth: 1 | |
| fetch-tags: true | |
| - name: Check release tag and draft state | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ACTION: ${{ inputs.release-action }} | |
| RELEASE_GIT_TAG: ${{ inputs.git-tag }} | |
| DRY_RUN_DOCS_BRANCH: ${{ inputs.dry-run-docs-branch }} | |
| run: | | |
| if [[ "$RELEASE_ACTION" == "full-release" && -n "$DRY_RUN_DOCS_BRANCH" ]]; then | |
| echo "error: dry-run-docs-branch is only valid with release-action=dry-run" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$RELEASE_ACTION" == "dry-run" && -n "$DRY_RUN_DOCS_BRANCH" && ! "$DRY_RUN_DOCS_BRANCH" =~ ^gh-pages-[[:alnum:]._/-]+$ ]]; then | |
| echo "error: dry-run-docs-branch must be a non-production gh-pages-* branch" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$RELEASE_ACTION" == "dry-run" ]]; then | |
| git rev-parse --verify "${RELEASE_GIT_TAG}^{commit}" | |
| echo "Dry-run selected; not checking or creating a GitHub release draft." | |
| exit 0 | |
| fi | |
| 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 }}" | |
| fi | |
| check-release-notes: | |
| if: ${{ github.repository_owner == 'nvidia' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.release-notes.outputs.version }} | |
| steps: | |
| - name: Checkout Source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.git-tag }} | |
| - name: Set up Python | |
| # $/ resolves at the workflow's commit. ./ would resolve against the | |
| # release tag checkout, which can predate this action. | |
| uses: $/.github/actions/setup-hardened-python | |
| with: | |
| python-version: "3.12" | |
| - name: Check versioned release notes exist | |
| id: release-notes | |
| env: | |
| RELEASE_GIT_TAG: ${{ inputs.git-tag }} | |
| RELEASE_COMPONENT: ${{ inputs.component }} | |
| BACKPORT_GIT_TAG: ${{ inputs.backport-git-tag }} | |
| run: | | |
| python ci/tools/check_release_notes.py \ | |
| --git-tag "$RELEASE_GIT_TAG" \ | |
| --component "$RELEASE_COMPONENT" \ | |
| --backport-git-tag "$BACKPORT_GIT_TAG" | |
| - name: Require Pathfinder version output | |
| if: ${{ inputs.component == 'cuda-pathfinder' }} | |
| env: | |
| VERSION: ${{ steps.release-notes.outputs.version }} | |
| run: | | |
| [[ -n "$VERSION" ]] || { | |
| echo "::error::The tagged Pathfinder release-note checker must emit a non-empty version output" | |
| exit 1 | |
| } | |
| ensure-pathfinder-draft: | |
| name: Prepare Pathfinder release draft | |
| if: ${{ github.repository_owner == 'nvidia' && inputs.component == 'cuda-pathfinder' && inputs.release-action == 'full-release' }} | |
| needs: [check-tag, check-release-notes] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| TAG: ${{ inputs.git-tag }} | |
| VERSION: ${{ needs.check-release-notes.outputs.version }} | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - name: Set metadata for a Pathfinder draft | |
| run: | | |
| [[ -n "$VERSION" ]] || { echo "::error::Empty version output"; exit 1; } | |
| # Recognize the shared placeholder across retries without overwriting curated drafts. | |
| is_placeholder=$(gh release view "$TAG" --repo "${{ github.repository }}" --json isDraft,name,body \ | |
| | jq -r --arg placeholder "Release ${TAG}" ' | |
| if .isDraft != true then error("Release is not a draft") | |
| else .name == $placeholder and .body == $placeholder end | |
| ') | |
| if [[ "$is_placeholder" != "true" ]]; then | |
| echo "Reusing draft release ${TAG}" | |
| exit 0 | |
| fi | |
| cat > "${RUNNER_TEMP}/release-body.md" <<BODY | |
| ## Release notes | |
| - https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/release/${VERSION}-notes.html | |
| ## Documentation | |
| - https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/ | |
| ## PyPI | |
| - https://pypi.org/project/cuda-pathfinder/${VERSION}/ | |
| ## Conda | |
| - https://anaconda.org/conda-forge/cuda-pathfinder/files?version=${VERSION} | |
| - \`conda install conda-forge::cuda-pathfinder=${VERSION}\` | |
| BODY | |
| gh release edit "$TAG" --repo "${{ github.repository }}" \ | |
| --latest=false \ | |
| --title "cuda-pathfinder v${VERSION}" \ | |
| --notes-file "${RUNNER_TEMP}/release-body.md" | |
| 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 | |
| - check-release-notes | |
| - determine-run-id | |
| secrets: inherit | |
| uses: ./.github/workflows/build-docs.yml | |
| with: | |
| component: ${{ inputs.component }} | |
| git-tag: ${{ inputs.git-tag }} | |
| run-id: ${{ needs.determine-run-id.outputs.run-id }} | |
| is-release: true | |
| deploy-docs: ${{ inputs.release-action == 'full-release' || inputs.dry-run-docs-branch != '' }} | |
| docs-branch: ${{ (inputs.release-action == 'dry-run' && inputs.dry-run-docs-branch) || 'gh-pages' }} | |
| upload-archive: | |
| name: Validate release artifacts | |
| permissions: | |
| contents: write | |
| needs: | |
| - check-tag | |
| - check-release-notes | |
| - determine-run-id | |
| - doc | |
| secrets: inherit | |
| uses: ./.github/workflows/release-upload.yml | |
| with: | |
| git-tag: ${{ inputs.git-tag }} | |
| run-id: ${{ needs.determine-run-id.outputs.run-id }} | |
| component: ${{ inputs.component }} | |
| dry-run: ${{ inputs.release-action == 'dry-run' }} | |
| publish-testpypi: | |
| name: Publish wheels to TestPyPI | |
| if: >- | |
| ${{ !cancelled() && inputs.release-action == 'full-release' && | |
| needs.check-tag.result == 'success' && | |
| needs.check-release-notes.result == 'success' && | |
| needs.determine-run-id.result == 'success' && | |
| needs.doc.result == 'success' && | |
| (inputs.component != 'cuda-pathfinder' || needs.ensure-pathfinder-draft.result == 'success') }} | |
| runs-on: ubuntu-latest | |
| needs: | |
| - check-tag | |
| - check-release-notes | |
| - determine-run-id | |
| - doc | |
| - ensure-pathfinder-draft | |
| environment: | |
| name: testpypi | |
| url: https://test.pypi.org/p/${{ inputs.component }}/ | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Checkout Source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Download component wheels | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ./ci/tools/download-wheels "${{ needs.determine-run-id.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" | |
| - name: Validate wheel versions for release tag | |
| run: | | |
| ./ci/tools/validate-release-wheels "${{ inputs.git-tag }}" "${{ inputs.component }}" "dist" | |
| - name: Publish package distributions to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| verify-testpypi: | |
| name: Verify Pathfinder on TestPyPI | |
| if: ${{ inputs.component == 'cuda-pathfinder' && inputs.release-action == 'full-release' }} | |
| needs: [check-release-notes, publish-testpypi] | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| env: | |
| VERSION: ${{ needs.check-release-notes.outputs.version }} | |
| INDEX_URL: https://test.pypi.org/simple/ | |
| EXTRA_INDEX_URL: https://pypi.org/simple/ | |
| INDEX_NAME: TestPyPI | |
| steps: &verify-pathfinder-steps | |
| - name: Require version output | |
| run: | | |
| [[ -n "$VERSION" ]] || { echo "::error::Empty version output"; exit 1; } | |
| - name: Create verification environment | |
| # These jobs have no checkout; $/ loads the action from the workflow's commit. | |
| uses: $/.github/actions/setup-hardened-python | |
| with: | |
| python-version: "3.12" | |
| venv: ${{ runner.temp }}/verify | |
| - name: Install Pathfinder from index and verify | |
| run: | | |
| source "${RUNNER_TEMP}/verify/bin/activate" | |
| index_args=() | |
| if [[ -n "$INDEX_URL" ]]; then | |
| index_args+=( --index-url "$INDEX_URL" ) | |
| fi | |
| if [[ -n "$EXTRA_INDEX_URL" ]]; then | |
| index_args+=( --extra-index-url "$EXTRA_INDEX_URL" ) | |
| fi | |
| max_attempts=6 | |
| retry_seconds=30 | |
| for ((attempt=1; attempt<=max_attempts; attempt++)); do | |
| if pip install "${index_args[@]}" "cuda-pathfinder==${VERSION}"; then | |
| break | |
| fi | |
| if (( attempt == max_attempts )); then | |
| echo "::error::Failed to install cuda-pathfinder==${VERSION} from ${INDEX_NAME} after ${max_attempts} attempts" | |
| exit 1 | |
| fi | |
| sleep "$retry_seconds" | |
| done | |
| installed=$(python -c "from cuda.pathfinder import __version__; print(__version__)") | |
| if [[ "$installed" != "$VERSION" ]]; then | |
| echo "::error::Version mismatch: expected ${VERSION}, got ${installed}" | |
| exit 1 | |
| fi | |
| publish-pypi: | |
| name: Publish wheels to PyPI | |
| if: >- | |
| ${{ !cancelled() && inputs.release-action == 'full-release' && | |
| needs.determine-run-id.result == 'success' && needs.publish-testpypi.result == 'success' && | |
| (inputs.component != 'cuda-pathfinder' || needs.verify-testpypi.result == 'success') }} | |
| runs-on: ubuntu-latest | |
| needs: | |
| - determine-run-id | |
| - publish-testpypi | |
| - verify-testpypi | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/${{ inputs.component }}/ | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Checkout Source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Download component wheels | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ./ci/tools/download-wheels "${{ needs.determine-run-id.outputs.run-id }}" "${{ inputs.component }}" "${{ github.repository }}" "dist" | |
| - name: Validate wheel versions for release tag | |
| run: | | |
| ./ci/tools/validate-release-wheels "${{ inputs.git-tag }}" "${{ inputs.component }}" "dist" | |
| - name: Publish package distributions to PyPI | |
| uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 | |
| verify-pypi: | |
| name: Verify Pathfinder on PyPI | |
| if: ${{ inputs.component == 'cuda-pathfinder' && inputs.release-action == 'full-release' }} | |
| needs: [check-release-notes, publish-pypi] | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| env: | |
| VERSION: ${{ needs.check-release-notes.outputs.version }} | |
| INDEX_URL: "" | |
| EXTRA_INDEX_URL: "" | |
| INDEX_NAME: PyPI | |
| steps: *verify-pathfinder-steps | |
| finalize-pathfinder: | |
| name: Publish Pathfinder GitHub release | |
| if: ${{ inputs.component == 'cuda-pathfinder' && inputs.release-action == 'full-release' }} | |
| needs: [doc, check-release-notes, ensure-pathfinder-draft, publish-pypi, verify-pypi, upload-archive] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| TAG: ${{ inputs.git-tag }} | |
| VERSION: ${{ needs.check-release-notes.outputs.version }} | |
| steps: | |
| - name: Verify docs URL | |
| run: | | |
| [[ -n "$VERSION" ]] || { echo "::error::Empty version output"; exit 1; } | |
| url="https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/" | |
| status=$(curl -sL -o /dev/null -w '%{http_code}' "$url") | |
| if [[ "$status" != "200" ]]; then | |
| echo "::warning::Docs URL returned HTTP ${status} -- docs may not be deployed yet" | |
| fi | |
| - name: Verify release is still a draft | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| is_draft=$(gh release view "$TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft') | |
| if [[ "$is_draft" != "true" ]]; then | |
| echo "::error::Release ${TAG} is no longer a draft (was it published manually?)" | |
| exit 1 | |
| fi | |
| - name: Publish release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false --latest=false | |
| docs_url="https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/" | |
| echo "$docs_url" | |
| printf '%s\n' "$docs_url" >> "$GITHUB_STEP_SUMMARY" |