Skip to content

cuda.bindings: support multiple CTK release lines on main #103

cuda.bindings: support multiple CTK release lines on main

cuda.bindings: support multiple CTK release lines on main #103

# 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}"