From a249027ba9be7a38030b2534fb2cf4f918b49de9 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 23 Oct 2026 09:26:19 -0700 Subject: [PATCH 1/2] CI: harden pip in the release workflow's Pathfinder verification jobs CodeRabbit noted on #3197 that verify-testpypi and verify-pypi create a venv and immediately run pip install, so the release checks use the venv-bootstrapped pip instead of the hash-pinned pip 26.2.2 that the other workflows now install. Split the shared verification steps so harden-pip runs against the venv's interpreter before the first install. The jobs have no checkout, so the action is referenced with the self-repository syntax ($/), which resolves at the workflow's commit; add the matching actionlint ignore (rhysd/actionlint#711). These jobs run only in full Pathfinder releases. Running their steps locally against cuda-pathfinder 1.8.4 on TestPyPI and PyPI installed pip 26.2.2 before the package install and passed the version check. Refs #3195, #3197 Signed-off-by: Ralf Juengling --- .github/actionlint.yaml | 3 +++ .github/workflows/release.yml | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index f7e4d55c7e9..7abe2ea7e56 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -23,6 +23,9 @@ paths: - 'unexpected key "queue" for "concurrency" section' # GitHub supports self-repository ($/) action references, but the latest # actionlint release does not yet recognize them (rhysd/actionlint#711). + ".github/workflows/release.yml": + ignore: + - 'specifying action "\$/[^"]+" in invalid format because ref is missing' ".github/workflows/test-wheel-linux.yml": ignore: - 'specifying action "\$/[^"]+" in invalid format because ref is missing' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5df00cca1df..8e762e142d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -344,10 +344,19 @@ jobs: EXTRA_INDEX_URL: https://pypi.org/simple/ INDEX_NAME: TestPyPI steps: &verify-pathfinder-steps - - name: Install Pathfinder from index and verify + - name: Create verification environment run: | [[ -n "$VERSION" ]] || { echo "::error::Empty version output"; exit 1; } python3 -m venv "${RUNNER_TEMP}/verify" + + - name: Harden pip + # These jobs have no checkout; $/ loads the action from the workflow's commit. + uses: $/.github/actions/harden-pip + with: + python: ${{ runner.temp }}/verify/bin/python + + - name: Install Pathfinder from index and verify + run: | source "${RUNNER_TEMP}/verify/bin/activate" index_args=() if [[ -n "$INDEX_URL" ]]; then From 598c60245d7fa7bbe4ff1cccdaaa9917f6051485 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 23 Oct 2026 09:27:03 -0700 Subject: [PATCH 2/2] CI: retire release-cuda-pathfinder.yml #3046 moved cuda-pathfinder releases into release.yml. The dedicated workflow was kept only for the transition: until release.yml was a trusted publisher on PyPI and TestPyPI, and for older tags whose release-note checker did not emit the version output. Neither reason applies anymore. The PyPI and TestPyPI attestations for cuda-pathfinder 1.8.4 name release.yml as the publisher, and every cuda-pathfinder-v* tag already has a published GitHub release. The old workflow last ran on Oct 2 for 1.8.3. Delete the workflow and the transition notes in release.yml and .github/RELEASE-core.md, as suggested on #3197. Refs #3197 Signed-off-by: Ralf Juengling --- .github/RELEASE-core.md | 10 - .github/workflows/release-cuda-pathfinder.yml | 387 ------------------ .github/workflows/release.yml | 2 - 3 files changed, 399 deletions(-) delete mode 100644 .github/workflows/release-cuda-pathfinder.yml diff --git a/.github/RELEASE-core.md b/.github/RELEASE-core.md index 6b6ee92df08..790c79c3e92 100644 --- a/.github/RELEASE-core.md +++ b/.github/RELEASE-core.md @@ -26,22 +26,12 @@ of `ci/tools/check_release_notes.py` that writes `version=` to `GITHUB_OUTPUT`. The release-notes job rejects an empty version before documentation, asset uploads, or package publication can start. Pathfinder package publication also requires successful draft metadata preparation. -Use the retained legacy Pathfinder workflow for older tags without version -output support during the transition; do not move existing tags to add the -updated checker. The default `dry-run` action validates preparation, builds docs, and collects release artifacts without creating a GitHub Release or publishing packages. It can be dispatched from a development branch; use `dry-run-docs-branch` only when a preview deployment is needed. -Before the first Pathfinder release through this workflow, its trusted -publishers on **both PyPI and TestPyPI** must authorize `release.yml`. -Until that migration is complete, `release-cuda-pathfinder.yml` remains -available. Keep its existing publisher -registrations during the transition; retire the old workflow and its -registrations after the consolidated workflow is authorized on both indexes. - Each section below provides detailed guidance for a step in the [Release Checklist](ISSUE_TEMPLATE/release_checklist.yml). To start a release, create a new issue from that template and work through it item by diff --git a/.github/workflows/release-cuda-pathfinder.yml b/.github/workflows/release-cuda-pathfinder.yml deleted file mode 100644 index 45d9fde1a10..00000000000 --- a/.github/workflows/release-cuda-pathfinder.yml +++ /dev/null @@ -1,387 +0,0 @@ -# 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 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 -# 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: - tag: - description: "Release tag to publish (e.g. cuda-pathfinder-v1.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: - # -------------------------------------------------------------------------- - # Collect release metadata, find the CI run, create a draft release. - # -------------------------------------------------------------------------- - prepare: - if: ${{ github.repository_owner == 'nvidia' }} - 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 }} - 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 - - - name: Set release variables - id: vars - env: - TAG_INPUT: ${{ inputs.tag }} - run: | - version="${TAG_INPUT#cuda-pathfinder-v}" - { - echo "tag=${TAG_INPUT}" - echo "version=${version}" - } >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # The exact tag supplies the SHA for lookup-run-id; no other tags - # or commit history are needed. - fetch-depth: 1 - # Resolve only the exact tag ref; checkout fails if the tag does not exist. - ref: refs/tags/${{ steps.vars.outputs.tag }} - - - 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: 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 - exit 0 - fi - cat > /tmp/release-body.md < "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: - - name: Download wheels - 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 - - - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 - 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 - max_attempts=6 - retry_seconds=30 - for ((attempt=1; attempt<=max_attempts; attempt++)); 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 == max_attempts )); then - echo "::error::Failed to install cuda-pathfinder==${VERSION} from TestPyPI 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 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: - - name: Download wheels - 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 - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 - - # -------------------------------------------------------------------------- - # 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 - max_attempts=6 - retry_seconds=30 - for ((attempt=1; attempt<=max_attempts; attempt++)); do - if pip install "cuda-pathfinder==${VERSION}"; then - break - fi - if (( attempt == max_attempts )); then - echo "::error::Failed to install cuda-pathfinder==${VERSION} from PyPI 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 - - # -------------------------------------------------------------------------- - # 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}/" - 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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e762e142d1..acefd058a58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,6 @@ name: "CI: Release" # 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. -# Before retiring the dedicated Pathfinder workflow, authorize release.yml in -# its PyPI and TestPyPI trusted publishers; see .github/RELEASE-core.md. # # Maintenance note: non-trivial changes to this workflow should be validated # with dry-run workflow_dispatch runs before merging. Suggested focused matrix: