Skip to content

Commit eb8c049

Browse files
cpcloudcursoragent
andcommitted
Add one-click release workflow for cuda-pathfinder
Automates the manual release checklist into a single workflow_dispatch that takes a version number. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3e022ad commit eb8c049

1 file changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# One-click release workflow for cuda-pathfinder.
6+
#
7+
# Provide a version number. The workflow automatically finds the CI run and
8+
# creates the git tag, creates a draft GitHub release with the standard
9+
# body, builds versioned docs, uploads source archive + wheels to the
10+
# release, publishes to TestPyPI, verifies the install, publishes to PyPI,
11+
# verifies again, and finally marks the release as published.
12+
13+
name: "Release: cuda-pathfinder"
14+
15+
on:
16+
workflow_dispatch:
17+
inputs:
18+
version:
19+
description: "Version to release (e.g. 1.3.5)"
20+
required: true
21+
type: string
22+
23+
concurrency:
24+
group: release-cuda-pathfinder
25+
cancel-in-progress: false
26+
27+
defaults:
28+
run:
29+
shell: bash --noprofile --norc -xeuo pipefail {0}
30+
31+
jobs:
32+
# --------------------------------------------------------------------------
33+
# Validate inputs, find the CI run, create the tag + draft release.
34+
# --------------------------------------------------------------------------
35+
prepare:
36+
runs-on: ubuntu-latest
37+
permissions:
38+
contents: write
39+
outputs:
40+
tag: ${{ steps.vars.outputs.tag }}
41+
version: ${{ steps.vars.outputs.version }}
42+
run-id: ${{ steps.detect-run.outputs.run-id }}
43+
ctk-ver: ${{ steps.ctk.outputs.ctk-ver }}
44+
steps:
45+
- name: Verify running on default branch
46+
run: |
47+
if [[ "${{ github.ref_name }}" != "${{ github.event.repository.default_branch }}" ]]; then
48+
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)."
49+
exit 1
50+
fi
51+
52+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
53+
with:
54+
fetch-depth: 0
55+
56+
- name: Validate version
57+
id: vars
58+
env:
59+
VERSION_INPUT: ${{ inputs.version }}
60+
run: |
61+
# Strip leading "v" if present (common typo)
62+
version="${VERSION_INPUT#v}"
63+
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
64+
echo "::error::Version must be MAJOR.MINOR.PATCH, got: ${version}"
65+
exit 1
66+
fi
67+
tag="cuda-pathfinder-v${version}"
68+
{
69+
echo "tag=${tag}"
70+
echo "version=${version}"
71+
} >> "$GITHUB_OUTPUT"
72+
73+
- name: Check release notes exist
74+
env:
75+
VERSION: ${{ steps.vars.outputs.version }}
76+
run: |
77+
notes="cuda_pathfinder/docs/source/release/${VERSION}-notes.rst"
78+
if [[ ! -f "${notes}" ]]; then
79+
echo "::error::Release notes not found: ${notes}"
80+
echo "Create the release notes file before running this workflow."
81+
exit 1
82+
fi
83+
84+
- name: Read CTK build version
85+
id: ctk
86+
run: |
87+
ctk_ver=$(yq '.cuda.build.version' ci/versions.yml)
88+
echo "ctk-ver=${ctk_ver}" >> "$GITHUB_OUTPUT"
89+
90+
- name: Create tag
91+
env:
92+
TAG: ${{ steps.vars.outputs.tag }}
93+
run: |
94+
if git rev-parse "${TAG}" >/dev/null 2>&1; then
95+
echo "Tag ${TAG} already exists"
96+
else
97+
git tag "${TAG}"
98+
git push origin "${TAG}"
99+
fi
100+
101+
- name: Detect CI run ID
102+
id: detect-run
103+
env:
104+
GH_TOKEN: ${{ github.token }}
105+
TAG: ${{ steps.vars.outputs.tag }}
106+
run: |
107+
run_id=$(./ci/tools/lookup-run-id "${TAG}" "${{ github.repository }}")
108+
echo "run-id=${run_id}" >> "$GITHUB_OUTPUT"
109+
110+
- name: Create draft release
111+
env:
112+
GH_TOKEN: ${{ github.token }}
113+
TAG: ${{ steps.vars.outputs.tag }}
114+
VERSION: ${{ steps.vars.outputs.version }}
115+
run: |
116+
# If the release exists and is already published, stop early.
117+
existing_draft=$(gh release view "${TAG}" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null || echo "missing")
118+
if [[ "${existing_draft}" == "false" ]]; then
119+
echo "::error::Release ${TAG} already exists and is published. Cannot re-release."
120+
exit 1
121+
fi
122+
if [[ "${existing_draft}" == "true" ]]; then
123+
echo "Draft release ${TAG} already exists, skipping creation"
124+
exit 0
125+
fi
126+
cat > /tmp/release-body.md <<BODY
127+
## Release notes
128+
129+
- https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/release/${VERSION}-notes.html
130+
131+
## Documentation
132+
133+
- https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/
134+
135+
## PyPI
136+
137+
- https://pypi.org/project/cuda-pathfinder/${VERSION}/
138+
139+
## Conda
140+
141+
- https://anaconda.org/conda-forge/cuda-pathfinder/files?version=${VERSION}
142+
- \`conda install conda-forge::cuda-pathfinder=${VERSION}\`
143+
BODY
144+
gh release create "${TAG}" \
145+
--repo "${{ github.repository }}" \
146+
--draft \
147+
--latest=false \
148+
--title "cuda-pathfinder v${VERSION}" \
149+
--notes-file /tmp/release-body.md
150+
151+
# --------------------------------------------------------------------------
152+
# Build and deploy versioned docs.
153+
# --------------------------------------------------------------------------
154+
docs:
155+
needs: prepare
156+
if: ${{ github.repository_owner == 'nvidia' }}
157+
permissions:
158+
id-token: write
159+
contents: write
160+
pull-requests: write
161+
secrets: inherit
162+
uses: ./.github/workflows/build-docs.yml
163+
with:
164+
build-ctk-ver: ${{ needs.prepare.outputs.ctk-ver }}
165+
component: cuda-pathfinder
166+
git-tag: ${{ needs.prepare.outputs.tag }}
167+
run-id: ${{ needs.prepare.outputs.run-id }}
168+
is-release: true
169+
170+
# --------------------------------------------------------------------------
171+
# Upload source archive and wheels to the GitHub release.
172+
# Runs even if docs fail -- assets are independent and the finalize
173+
# job's docs-URL check will warn if docs aren't deployed yet.
174+
# --------------------------------------------------------------------------
175+
upload-assets:
176+
needs: [prepare, docs]
177+
if: ${{ !cancelled() && needs.prepare.result == 'success' }}
178+
runs-on: ubuntu-latest
179+
permissions:
180+
contents: write
181+
env:
182+
TAG: ${{ needs.prepare.outputs.tag }}
183+
RUN_ID: ${{ needs.prepare.outputs.run-id }}
184+
steps:
185+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
186+
with:
187+
fetch-depth: 0
188+
ref: ${{ needs.prepare.outputs.tag }}
189+
190+
- name: Create source archive
191+
run: |
192+
archive="${{ github.event.repository.name }}-${TAG}"
193+
mkdir -p release
194+
git archive \
195+
--format=tar.gz \
196+
--prefix="${archive}/" \
197+
--output="release/${archive}.tar.gz" \
198+
"${TAG}"
199+
sha256sum "release/${archive}.tar.gz" \
200+
| awk '{print $1}' > "release/${archive}.tar.gz.sha256sum"
201+
202+
- name: Download wheels
203+
env:
204+
GH_TOKEN: ${{ github.token }}
205+
run: |
206+
./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "release/wheels"
207+
208+
- name: Upload to release
209+
env:
210+
GH_TOKEN: ${{ github.token }}
211+
run: |
212+
gh release upload "${TAG}" \
213+
--repo "${{ github.repository }}" \
214+
--clobber \
215+
release/*.tar.gz release/*.sha256sum release/wheels/*.whl
216+
217+
# --------------------------------------------------------------------------
218+
# Publish to TestPyPI.
219+
# --------------------------------------------------------------------------
220+
publish-testpypi:
221+
needs: [prepare, docs]
222+
if: ${{ !cancelled() && needs.prepare.result == 'success' }}
223+
runs-on: ubuntu-latest
224+
environment:
225+
name: testpypi
226+
url: https://test.pypi.org/p/cuda-pathfinder/
227+
permissions:
228+
id-token: write
229+
env:
230+
RUN_ID: ${{ needs.prepare.outputs.run-id }}
231+
steps:
232+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
233+
234+
- name: Download wheels
235+
env:
236+
GH_TOKEN: ${{ github.token }}
237+
run: |
238+
./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "dist"
239+
240+
- name: Publish to TestPyPI
241+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
242+
with:
243+
repository-url: https://test.pypi.org/legacy/
244+
245+
# --------------------------------------------------------------------------
246+
# Verify the TestPyPI package installs and imports correctly.
247+
# --------------------------------------------------------------------------
248+
verify-testpypi:
249+
needs: [prepare, publish-testpypi]
250+
runs-on: ubuntu-latest
251+
env:
252+
VERSION: ${{ needs.prepare.outputs.version }}
253+
steps:
254+
- name: Install from TestPyPI and verify
255+
run: |
256+
python3 -m venv /tmp/verify
257+
source /tmp/verify/bin/activate
258+
for attempt in 1 2 3 4 5 6; do
259+
if pip install \
260+
--index-url https://test.pypi.org/simple/ \
261+
--extra-index-url https://pypi.org/simple/ \
262+
"cuda-pathfinder==${VERSION}"; then
263+
break
264+
fi
265+
if [[ "${attempt}" -eq 6 ]]; then
266+
echo "::error::Failed to install cuda-pathfinder==${VERSION} from TestPyPI after 6 attempts"
267+
exit 1
268+
fi
269+
echo "Attempt ${attempt}: not available yet, retrying in 30s..."
270+
sleep 30
271+
done
272+
installed=$(python -c "from cuda.pathfinder import __version__; print(__version__)")
273+
if [[ "${installed}" != "${VERSION}" ]]; then
274+
echo "::error::Version mismatch: expected ${VERSION}, got ${installed}"
275+
exit 1
276+
fi
277+
echo "TestPyPI verification passed: cuda-pathfinder==${installed}"
278+
279+
# --------------------------------------------------------------------------
280+
# Publish to PyPI.
281+
# --------------------------------------------------------------------------
282+
publish-pypi:
283+
needs: [prepare, verify-testpypi]
284+
runs-on: ubuntu-latest
285+
environment:
286+
name: pypi
287+
url: https://pypi.org/p/cuda-pathfinder/
288+
permissions:
289+
id-token: write
290+
env:
291+
RUN_ID: ${{ needs.prepare.outputs.run-id }}
292+
steps:
293+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
294+
295+
- name: Download wheels
296+
env:
297+
GH_TOKEN: ${{ github.token }}
298+
run: |
299+
./ci/tools/download-wheels "${RUN_ID}" "cuda-pathfinder" "${{ github.repository }}" "dist"
300+
301+
- name: Publish to PyPI
302+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
303+
304+
# --------------------------------------------------------------------------
305+
# Verify the PyPI package installs and imports correctly.
306+
# --------------------------------------------------------------------------
307+
verify-pypi:
308+
needs: [prepare, publish-pypi]
309+
runs-on: ubuntu-latest
310+
env:
311+
VERSION: ${{ needs.prepare.outputs.version }}
312+
steps:
313+
- name: Install from PyPI and verify
314+
run: |
315+
python3 -m venv /tmp/verify
316+
source /tmp/verify/bin/activate
317+
for attempt in 1 2 3 4 5 6; do
318+
if pip install "cuda-pathfinder==${VERSION}"; then
319+
break
320+
fi
321+
if [[ "${attempt}" -eq 6 ]]; then
322+
echo "::error::Failed to install cuda-pathfinder==${VERSION} from PyPI after 6 attempts"
323+
exit 1
324+
fi
325+
echo "Attempt ${attempt}: not available yet, retrying in 30s..."
326+
sleep 30
327+
done
328+
installed=$(python -c "from cuda.pathfinder import __version__; print(__version__)")
329+
if [[ "${installed}" != "${VERSION}" ]]; then
330+
echo "::error::Version mismatch: expected ${VERSION}, got ${installed}"
331+
exit 1
332+
fi
333+
echo "PyPI verification passed: cuda-pathfinder==${installed}"
334+
335+
# --------------------------------------------------------------------------
336+
# Verify docs and publish the release (mark non-draft).
337+
# --------------------------------------------------------------------------
338+
finalize:
339+
needs: [prepare, verify-pypi, upload-assets]
340+
runs-on: ubuntu-latest
341+
permissions:
342+
contents: write
343+
env:
344+
TAG: ${{ needs.prepare.outputs.tag }}
345+
VERSION: ${{ needs.prepare.outputs.version }}
346+
steps:
347+
- name: Verify docs URL
348+
run: |
349+
url="https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/"
350+
echo "Checking ${url}"
351+
for attempt in 1 2 3 4 5; do
352+
status=$(curl -sL -o /dev/null -w '%{http_code}' "${url}")
353+
if [[ "${status}" == "200" ]]; then
354+
echo "Docs URL is live"
355+
exit 0
356+
fi
357+
echo "Attempt ${attempt}: HTTP ${status}, retrying in 30s..."
358+
sleep 30
359+
done
360+
echo "::warning::Docs URL returned HTTP ${status} after 5 attempts -- docs may not be deployed yet"
361+
362+
- name: Verify release is still a draft
363+
env:
364+
GH_TOKEN: ${{ github.token }}
365+
run: |
366+
is_draft=$(gh release view "${TAG}" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft')
367+
if [[ "${is_draft}" != "true" ]]; then
368+
echo "::error::Release ${TAG} is no longer a draft (was it published manually?)"
369+
exit 1
370+
fi
371+
372+
- name: Publish release
373+
env:
374+
GH_TOKEN: ${{ github.token }}
375+
run: |
376+
gh release edit "${TAG}" \
377+
--repo "${{ github.repository }}" \
378+
--draft=false \
379+
--latest=false
380+
url="https://github.com/${{ github.repository }}/releases/tag/${TAG}"
381+
echo "Release ${TAG} published: ${url}"
382+
{
383+
echo "### cuda-pathfinder v${VERSION} released"
384+
echo ""
385+
echo "- **Release**: ${url}"
386+
echo "- **PyPI**: https://pypi.org/project/cuda-pathfinder/${VERSION}/"
387+
echo "- **Docs**: https://nvidia.github.io/cuda-python/cuda-pathfinder/${VERSION}/"
388+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)