Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 388 additions & 0 deletions .github/workflows/release-cuda-pathfinder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

# One-click release workflow for cuda-pathfinder.
#
# Provide a version number. The workflow automatically finds the CI run and
# creates the git tag, creates a draft GitHub release with the standard
# body, builds versioned docs, 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.

name: "Release: cuda-pathfinder"

on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g. 1.3.5)"
required: true
type: string

concurrency:
group: release-cuda-pathfinder
cancel-in-progress: false

defaults:
run:
shell: bash --noprofile --norc -xeuo pipefail {0}

jobs:
# --------------------------------------------------------------------------
# Validate inputs, find the CI run, create the tag + draft release.
# --------------------------------------------------------------------------
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.vars.outputs.tag }}
version: ${{ steps.vars.outputs.version }}
run-id: ${{ steps.detect-run.outputs.run-id }}
ctk-ver: ${{ steps.ctk.outputs.ctk-ver }}
steps:
- 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 }} (select the correct branch in the 'Use workflow from' dropdown)."
exit 1
fi

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Validate version
id: vars
env:
VERSION_INPUT: ${{ inputs.version }}
run: |
# Strip leading "v" if present (common typo)
version="${VERSION_INPUT#v}"
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version must be MAJOR.MINOR.PATCH, got: ${version}"
exit 1
fi
tag="cuda-pathfinder-v${version}"
{
echo "tag=${tag}"
echo "version=${version}"
} >> "$GITHUB_OUTPUT"

- name: Check release notes exist
env:
VERSION: ${{ steps.vars.outputs.version }}
run: |
notes="cuda_pathfinder/docs/source/release/${VERSION}-notes.rst"
if [[ ! -f "${notes}" ]]; then
echo "::error::Release notes not found: ${notes}"
echo "Create the release notes file before running this workflow."
exit 1
fi

- name: Read CTK build version
id: ctk
run: |
ctk_ver=$(yq '.cuda.build.version' ci/versions.yml)
echo "ctk-ver=${ctk_ver}" >> "$GITHUB_OUTPUT"

- name: Create tag
env:
TAG: ${{ steps.vars.outputs.tag }}
run: |
if git rev-parse "${TAG}" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists"
else
git tag "${TAG}"
git push origin "${TAG}"
fi

- name: Detect CI run ID
id: detect-run
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"

- name: Create draft release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.vars.outputs.tag }}
VERSION: ${{ steps.vars.outputs.version }}
run: |
# If the release exists and is already published, stop early.
existing_draft=$(gh release view "${TAG}" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null || echo "missing")
if [[ "${existing_draft}" == "false" ]]; then
echo "::error::Release ${TAG} already exists and is published. Cannot re-release."
exit 1
fi
if [[ "${existing_draft}" == "true" ]]; then
echo "Draft release ${TAG} already exists, skipping creation"
exit 0
fi
cat > /tmp/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 create "${TAG}" \
--repo "${{ github.repository }}" \
--draft \
--latest=false \
--title "cuda-pathfinder v${VERSION}" \
--notes-file /tmp/release-body.md

# --------------------------------------------------------------------------
# Build and deploy versioned docs.
# --------------------------------------------------------------------------
docs:
needs: prepare
if: ${{ github.repository_owner == 'nvidia' }}
permissions:
id-token: write
contents: write
pull-requests: write
secrets: inherit
uses: ./.github/workflows/build-docs.yml
with:
build-ctk-ver: ${{ needs.prepare.outputs.ctk-ver }}
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]
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

# --------------------------------------------------------------------------
# Publish to TestPyPI.
# --------------------------------------------------------------------------
publish-testpypi:
needs: [prepare, docs]
if: ${{ !cancelled() && needs.prepare.result == 'success' }}
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/cuda-pathfinder/
permissions:
id-token: write
env:
RUN_ID: ${{ needs.prepare.outputs.run-id }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Download wheels
env:
GH_TOKEN: ${{ github.token }}
run: |
./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "dist"

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: https://test.pypi.org/legacy/

# --------------------------------------------------------------------------
# Verify the TestPyPI package installs and imports correctly.
# --------------------------------------------------------------------------
verify-testpypi:
needs: [prepare, publish-testpypi]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Install from TestPyPI and verify
run: |
python3 -m venv /tmp/verify
source /tmp/verify/bin/activate
for attempt in 1 2 3 4 5 6; do
if pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"cuda-pathfinder==${VERSION}"; then
break
fi
if [[ "${attempt}" -eq 6 ]]; then
echo "::error::Failed to install cuda-pathfinder==${VERSION} from TestPyPI after 6 attempts"
exit 1
fi
echo "Attempt ${attempt}: not available yet, retrying in 30s..."
sleep 30
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
echo "TestPyPI verification passed: cuda-pathfinder==${installed}"

# --------------------------------------------------------------------------
# Publish to PyPI.
# --------------------------------------------------------------------------
publish-pypi:
needs: [prepare, verify-testpypi]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/cuda-pathfinder/
permissions:
id-token: write
env:
RUN_ID: ${{ needs.prepare.outputs.run-id }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Download wheels
env:
GH_TOKEN: ${{ github.token }}
run: |
./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "dist"

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0

# --------------------------------------------------------------------------
# Verify the PyPI package installs and imports correctly.
# --------------------------------------------------------------------------
verify-pypi:
needs: [prepare, publish-pypi]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Install from PyPI and verify
run: |
python3 -m venv /tmp/verify
source /tmp/verify/bin/activate
for attempt in 1 2 3 4 5 6; do
if pip install "cuda-pathfinder==${VERSION}"; then
break
fi
if [[ "${attempt}" -eq 6 ]]; then
echo "::error::Failed to install cuda-pathfinder==${VERSION} from PyPI after 6 attempts"
exit 1
fi
echo "Attempt ${attempt}: not available yet, retrying in 30s..."
sleep 30
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
echo "PyPI verification passed: cuda-pathfinder==${installed}"

# --------------------------------------------------------------------------
# Verify docs and publish the release (mark non-draft).
# --------------------------------------------------------------------------
finalize:
needs: [prepare, verify-pypi, upload-assets]
runs-on: ubuntu-latest
permissions:
contents: write
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Verify docs URL
run: |
url="https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/"
echo "Checking ${url}"
for attempt in 1 2 3 4 5; do
status=$(curl -sL -o /dev/null -w '%{http_code}' "${url}")
if [[ "${status}" == "200" ]]; then
echo "Docs URL is live"
exit 0
fi
echo "Attempt ${attempt}: HTTP ${status}, retrying in 30s..."
sleep 30
done
echo "::warning::Docs URL returned HTTP ${status} after 5 attempts -- docs may not be deployed yet"

- 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
url="https://github.com/${{ github.repository }}/releases/tag/${TAG}"
echo "Release ${TAG} published: ${url}"
{
echo "### cuda-pathfinder v${VERSION} released"
echo ""
echo "- **Release**: ${url}"
echo "- **PyPI**: https://pypi.org/project/cuda-pathfinder/${VERSION}/"
echo "- **Docs**: https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/"
} >> "$GITHUB_STEP_SUMMARY"