cuda.bindings: support multiple CTK release lines on main #103
Workflow file for this run
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) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # Fails when a committed pixi.lock is out of date with its pixi.toml, or when | |
| # its bytes are not what the pinned pixi version generates. Pull requests also | |
| # check stale workspaces at the base SHA so diagnostics can distinguish changes | |
| # introduced by the PR from maintenance already needed on the base branch. | |
| # Attribution changes remediation only; the aggregate job remains strict for | |
| # every stale classification. See #2298 and #2926. | |
| name: "CI: pixi lockfile freshness check" | |
| concurrency: | |
| # Keyed on the event as well as the ref so a manual dispatch and a push to | |
| # the same branch do not cancel each other. | |
| group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} | |
| cancel-in-progress: true | |
| on: | |
| pull_request: | |
| # `pyproject.toml` is a lockfile input too: the manifests consume sibling | |
| # packages (including cuda_python_test_helpers) as path dependencies, so | |
| # their metadata can stale a lock without any pixi.toml edit. Matched by | |
| # glob rather than by name so the filter cannot drift as packages move. | |
| paths: &lockfile_inputs | |
| - "**/pixi.toml" | |
| - "**/pixi.lock" | |
| - "**/pyproject.toml" | |
| - "ci/tools/classify_pixi_lockfile_freshness.py" | |
| - "ci/tools/list_pixi_workspaces.py" | |
| - "ci/pixi-version.env" | |
| - ".github/actions/setup-pixi/action.yml" | |
| - ".github/workflows/ci-pixi-lockfile-freshness-check.yml" | |
| push: | |
| # `pull_request` already covers PRs, including those from forks: this check | |
| # needs no secrets or GPU runner. Watching copy-pr-bot's `pull-request/N` | |
| # mirror too would run the whole check a second time per PR. | |
| branches: | |
| - "main" | |
| paths: *lockfile_inputs | |
| workflow_dispatch: {} | |
| defaults: | |
| run: | |
| shell: bash --noprofile --norc -xeuo pipefail {0} | |
| env: | |
| REFRESH_WORKFLOW_URL: "https://github.com/NVIDIA/cuda-python/actions/workflows/ci-pixi-lockfile-refresh.yml" | |
| permissions: {} | |
| jobs: | |
| lockfile-fresh: | |
| name: pixi lock --check (all workspaces) | |
| if: ${{ github.repository_owner == 'nvidia' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 135 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - name: Checkout ${{ github.event.repository.name }} | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup pixi | |
| id: setup-pixi | |
| uses: ./.github/actions/setup-pixi | |
| - name: Find open lockfile refresh PR | |
| id: maintenance | |
| if: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| lookup_status=0 | |
| refresh_url="$( | |
| gh pr list \ | |
| --state open \ | |
| --head ci/pixi-lock-refresh/all \ | |
| --json url,isCrossRepository \ | |
| --jq '[.[] | select(.isCrossRepository == false)][0].url // ""' | |
| )" || lookup_status=$? | |
| if ((lookup_status != 0)); then | |
| echo "::warning::Could not look up the open Pixi lockfile refresh PR; using the refresh workflow link instead." | |
| refresh_url="" | |
| fi | |
| if [[ -z "${refresh_url}" ]]; then | |
| refresh_url="${REFRESH_WORKFLOW_URL}" | |
| maintenance_kind="workflow" | |
| else | |
| maintenance_kind="pull-request" | |
| fi | |
| echo "url=${refresh_url}" >> "${GITHUB_OUTPUT}" | |
| echo "kind=${maintenance_kind}" >> "${GITHUB_OUTPUT}" | |
| # `pixi lock --check` exits 0 on a semantically current lock even when it | |
| # rewrites the file into the pinned version's canonical form (lockfile | |
| # format upgrades, platform alias renames). Without this guard that drift | |
| # is invisible: the check prints "Updated lock file" and still passes, | |
| # while every later pixi run keeps rewriting the committed file (#2298). | |
| - name: Check all lockfiles | |
| id: check | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| MAINTENANCE_KIND: ${{ steps.maintenance.outputs.kind || 'workflow' }} | |
| MAINTENANCE_URL: ${{ steps.maintenance.outputs.url || env.REFRESH_WORKFLOW_URL }} | |
| PIXI_VERSION: ${{ steps.setup-pixi.outputs.pixi-version }} | |
| WORKSPACE_TIMEOUT: 20m | |
| run: | | |
| workspaces="$(python3 ci/tools/list_pixi_workspaces.py)" | |
| workspace_rows="$( | |
| jq -ce ' | |
| if type != "array" or length == 0 then | |
| error("workspace inventory must be a non-empty array") | |
| elif any(.[]; | |
| type != "object" | |
| or (.manifest | type != "string") | |
| or (.lockfile | type != "string") | |
| or .manifest == "" | |
| or .lockfile == "" | |
| ) then | |
| error("each workspace must have non-empty string manifest and lockfile fields") | |
| else | |
| .[] | |
| end | |
| ' <<<"${workspaces}" | |
| )" | |
| failed=0 | |
| base_ready=0 | |
| base_parent="" | |
| base_worktree="" | |
| if [[ "${MAINTENANCE_KIND}" == "pull-request" ]]; then | |
| maintenance_annotation="Merge the open all-workspace lockfile refresh PR at ${MAINTENANCE_URL} and then update this branch and rerun CI." | |
| maintenance_summary="Merge the open [all-workspace lockfile refresh PR](${MAINTENANCE_URL}) and then update this branch and rerun CI." | |
| else | |
| maintenance_annotation="No all-workspace lockfile refresh PR is open. Open ${MAINTENANCE_URL} and select Run workflow. Run it against the default branch, merge the generated lockfile-refresh PR, update this branch, and rerun CI." | |
| maintenance_summary="No all-workspace refresh PR is open. Open the [Pixi lockfile refresh workflow](${MAINTENANCE_URL}) and select **Run workflow**. Run it against the default branch, merge the generated lockfile-refresh PR, update this branch, and rerun CI." | |
| fi | |
| { | |
| echo "### Pixi lockfile freshness" | |
| echo | |
| echo "| Workspace | Result |" | |
| echo "| --- | --- |" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| if [[ "${EVENT_NAME}" == "pull_request" ]]; then | |
| if [[ -z "${BASE_SHA}" ]]; then | |
| echo "::error::The pull request base SHA is unavailable, so stale lockfiles cannot be attributed. Rerun the workflow from a pull_request event; if the SHA is still missing, inspect the event payload and workflow configuration." | |
| echo "| Workflow | Operational error: base SHA unavailable. Rerun from a pull-request event; if it remains missing, inspect the event payload and workflow configuration. |" >> "${GITHUB_STEP_SUMMARY}" | |
| echo "remediation-printed=true" >> "${GITHUB_OUTPUT}" | |
| exit 1 | |
| fi | |
| base_parent="$(mktemp -d "${RUNNER_TEMP}/pixi-lockfile-base.XXXXXX")" | |
| base_worktree="${base_parent}/tree" | |
| fi | |
| while IFS= read -r workspace; do | |
| manifest="$(jq -r '.manifest' <<<"${workspace}")" | |
| lockfile="$(jq -r '.lockfile' <<<"${workspace}")" | |
| echo "::group::pixi lock --check (${manifest})" | |
| candidate_original="$(git hash-object "${lockfile}")" | |
| check_status=0 | |
| timeout --kill-after=1m "${WORKSPACE_TIMEOUT}" \ | |
| pixi lock --check --manifest-path "${manifest}" \ | |
| || check_status=$? | |
| candidate_repaired="$(git hash-object "${lockfile}")" | |
| if ((check_status == 124 || check_status == 137)); then | |
| echo "::error::Timed out after ${WORKSPACE_TIMEOUT} while checking '${manifest}'. Rerun the job. If it times out again, run 'pixi lock --check --manifest-path ${manifest}' with pixi ${PIXI_VERSION} and investigate solver, network, or package-index availability." | |
| echo "| \`${manifest}\` | Operational error: candidate check timed out. Rerun the job; if it repeats, run \`pixi lock --check --manifest-path ${manifest}\` with pixi ${PIXI_VERSION} and investigate solver, network, or package-index availability. |" >> "${GITHUB_STEP_SUMMARY}" | |
| failed=1 | |
| echo "::endgroup::" | |
| continue | |
| elif ((check_status != 0)) && [[ "${candidate_original}" == "${candidate_repaired}" ]]; then | |
| echo "::error::pixi ${PIXI_VERSION} failed while checking '${manifest}' (exit ${check_status}) without producing a repaired lockfile. Resolve the Pixi error printed above and rerun the job; do not commit a lockfile change unless a later run classifies it as PR-induced." | |
| echo "| \`${manifest}\` | Operational error: candidate check failed with exit ${check_status}. Resolve the Pixi error above and rerun; do not commit a lockfile change unless a later run classifies it as PR-induced. |" >> "${GITHUB_STEP_SUMMARY}" | |
| failed=1 | |
| echo "::endgroup::" | |
| continue | |
| elif ((check_status == 0)) && [[ "${candidate_original}" == "${candidate_repaired}" ]]; then | |
| echo "| \`${manifest}\` | Fresh |" >> "${GITHUB_STEP_SUMMARY}" | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| if [[ "${EVENT_NAME}" != "pull_request" ]]; then | |
| echo "::error title=Stale Pixi lockfile::Lockfile '${lockfile}' is stale. Regenerate and commit it with pixi ${PIXI_VERSION}: pixi lock --manifest-path ${manifest}." | |
| echo "| \`${manifest}\` | Stale: regenerate and commit the lockfile. |" >> "${GITHUB_STEP_SUMMARY}" | |
| failed=1 | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| if ((base_ready == 0)); then | |
| git worktree add --detach "${base_worktree}" "${BASE_SHA}" | |
| base_ready=1 | |
| fi | |
| base_manifest="${base_worktree}/${manifest}" | |
| base_lockfile="${base_worktree}/${lockfile}" | |
| if [[ ! -f "${base_manifest}/pixi.toml" || ! -f "${base_lockfile}" ]]; then | |
| base_result="missing" | |
| base_original="" | |
| base_repaired="" | |
| else | |
| base_original="$(git -C "${base_worktree}" hash-object "${lockfile}")" | |
| base_status=0 | |
| timeout --kill-after=1m "${WORKSPACE_TIMEOUT}" \ | |
| pixi lock --check --manifest-path "${base_manifest}" \ | |
| || base_status=$? | |
| base_repaired="$(git -C "${base_worktree}" hash-object "${lockfile}")" | |
| if ((base_status == 124 || base_status == 137)); then | |
| echo "::error::Timed out after ${WORKSPACE_TIMEOUT} while checking '${manifest}' at base ${BASE_SHA}. Rerun the job. If it times out again, check out ${BASE_SHA}, run 'pixi lock --check --manifest-path ${manifest}' with pixi ${PIXI_VERSION}, and resolve the operational problem before changing this PR's lockfile." | |
| echo "| \`${manifest}\` | Operational error: base check timed out. Rerun the job; if it repeats, check out \`${BASE_SHA}\`, run \`pixi lock --check --manifest-path ${manifest}\` with pixi ${PIXI_VERSION}, and resolve the operational problem before changing this PR's lockfile. |" >> "${GITHUB_STEP_SUMMARY}" | |
| failed=1 | |
| echo "::endgroup::" | |
| continue | |
| elif ((base_status != 0)) && [[ "${base_original}" == "${base_repaired}" ]]; then | |
| echo "::error::pixi ${PIXI_VERSION} failed while checking '${manifest}' at base ${BASE_SHA} (exit ${base_status}) without producing a repaired lockfile. Resolve the Pixi error printed above and rerun the job; do not change this PR's lockfile until base attribution succeeds." | |
| echo "| \`${manifest}\` | Operational error: base check failed with exit ${base_status}. Resolve the Pixi error above and rerun; do not change this PR's lockfile until base attribution succeeds. |" >> "${GITHUB_STEP_SUMMARY}" | |
| failed=1 | |
| echo "::endgroup::" | |
| continue | |
| elif ((base_status == 0)) && [[ "${base_original}" == "${base_repaired}" ]]; then | |
| base_result="fresh" | |
| else | |
| base_result="stale" | |
| fi | |
| fi | |
| classifier_args=( | |
| --candidate-original "${candidate_original}" | |
| --candidate-repaired "${candidate_repaired}" | |
| --base-result "${base_result}" | |
| ) | |
| if [[ "${base_result}" != "missing" ]]; then | |
| classifier_args+=( | |
| --base-original "${base_original}" | |
| --base-repaired "${base_repaired}" | |
| ) | |
| fi | |
| classification="$( | |
| python3 ci/tools/classify_pixi_lockfile_freshness.py "${classifier_args[@]}" | |
| )" | |
| case "${classification}" in | |
| pr-induced) | |
| echo "::error title=PR-induced Pixi lockfile change::Lockfile '${lockfile}' became stale in this PR. Regenerate and commit it with pixi ${PIXI_VERSION}: pixi lock --manifest-path ${manifest}." | |
| echo "| \`${manifest}\` | PR-induced: regenerate and commit the lockfile in this PR. |" >> "${GITHUB_STEP_SUMMARY}" | |
| ;; | |
| base-maintenance) | |
| echo "::error title=Base-maintenance Pixi lockfile change::Lockfile '${lockfile}' was already stale on the PR base; its original and repaired blobs match the candidate. Do not add this refresh to the feature PR. ${maintenance_annotation}" | |
| echo "| \`${manifest}\` | Base maintenance: not introduced by this PR. Do not add this refresh to the feature PR. ${maintenance_summary} |" >> "${GITHUB_STEP_SUMMARY}" | |
| ;; | |
| mixed-or-ambiguous) | |
| echo "::error title=Mixed or ambiguous Pixi lockfile change::Lockfile '${lockfile}' is stale on both the PR base and candidate, but their original or repaired blobs differ. Refresh the base first. ${maintenance_annotation} Then address any remaining PR-specific lockfile change." | |
| echo "| \`${manifest}\` | Mixed or ambiguous: refresh the base first. ${maintenance_summary} Then address any remaining PR-specific lockfile change. |" >> "${GITHUB_STEP_SUMMARY}" | |
| ;; | |
| *) | |
| echo "::error::Unknown lockfile freshness classification '${classification}' for '${manifest}'. Rerun the job; if it repeats, report a workflow bug with this classification and the candidate/base blob IDs printed above." | |
| echo "| \`${manifest}\` | Operational error: unknown classification \`${classification}\`. Rerun the job; if it repeats, report a workflow bug with the classification and blob IDs from the log. |" >> "${GITHUB_STEP_SUMMARY}" | |
| ;; | |
| esac | |
| failed=1 | |
| echo "::endgroup::" | |
| done <<<"${workspace_rows}" | |
| if ((base_ready != 0)); then | |
| git worktree remove --force "${base_worktree}" | |
| fi | |
| if [[ -n "${base_parent}" ]]; then | |
| rmdir "${base_parent}" | |
| fi | |
| if ((failed != 0)); then | |
| echo "remediation-printed=true" >> "${GITHUB_OUTPUT}" | |
| fi | |
| exit "${failed}" | |
| # Covers checkout, setup, inventory, Git, and other unexpected failures | |
| # that occur before the check can emit category-specific remediation. | |
| - name: Report fallback remediation | |
| if: ${{ failure() && steps.check.outputs.remediation-printed != 'true' }} | |
| run: | | |
| echo "::error title=Pixi freshness check remediation::An earlier step failed before category-specific remediation could be produced. Rerun the job once. If it fails again, resolve the first operational error in the log. For base-maintenance drift, open ${REFRESH_WORKFLOW_URL} and select Run workflow. Run it against the default branch, merge the generated lockfile-refresh PR, update this branch, and rerun CI." | |
| { | |
| echo "### Pixi freshness check remediation" | |
| echo | |
| echo "The job failed before category-specific remediation could be produced:" | |
| echo | |
| echo "1. Rerun the job once to rule out a transient checkout, network, package-index, or solver failure." | |
| echo "2. If it fails again, resolve the first operational error in the log." | |
| echo "3. For base-maintenance drift, open the [Pixi lockfile refresh workflow](${REFRESH_WORKFLOW_URL}) and select **Run workflow**. Run it against the default branch, merge the generated lockfile-refresh PR, update this branch, and rerun CI." | |
| echo "4. For a PR-induced stale lockfile, regenerate the affected lockfile with the pinned Pixi version, commit it to this PR, and rerun CI." | |
| } >> "${GITHUB_STEP_SUMMARY}" |