Skip to content
Merged
Show file tree
Hide file tree
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
48 changes: 46 additions & 2 deletions .github/workflows/ci-pixi-lockfile-freshness-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
# 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.
# Attribution changes remediation only. A temporary, exact-match exception
# covers Pixi 0.73.0 pruning the samples environment's local source references;
# every other stale classification remains strict. See #2298 and #2926.

name: "CI: pixi lockfile freshness check"

Expand All @@ -28,6 +29,8 @@ on:
- "**/pixi.lock"
- "**/pyproject.toml"
- "ci/tools/classify_pixi_lockfile_freshness.py"
- "ci/tools/check_pixi_samples_source_pruning.py"
- "ci/tools/tests/test_check_pixi_samples_source_pruning.py"
- "ci/tools/list_pixi_workspaces.py"
- "ci/pixi-version.env"
- ".github/actions/setup-pixi/action.yml"
Expand Down Expand Up @@ -195,7 +198,35 @@ jobs:
continue
fi

known_samples_pruning=0
if [[ "${manifest}" == "cuda_core" ]]; then
defect_status=0
python3 ci/tools/check_pixi_samples_source_pruning.py \
--original-blob "${candidate_original}" \
--lockfile "${lockfile}" \
--manifest-path "${manifest}/pixi.toml" \
--pixi-version "${PIXI_VERSION}" \
--check-status "${check_status}" \
|| defect_status=$?
if ((defect_status == 0)); then
known_samples_pruning=1
elif ((defect_status != 1)); then
echo "::error::Could not determine whether '${lockfile}' matches the known Pixi samples-source pruning defect. Resolve the detector error above and rerun."
echo "| \`${manifest}\` | Operational error: known-defect detector failed. |" >> "${GITHUB_STEP_SUMMARY}"
failed=1
echo "::endgroup::"
continue
fi
fi

if [[ "${EVENT_NAME}" != "pull_request" ]]; then
if ((known_samples_pruning != 0)); then
echo "::warning title=Known Pixi samples lockfile bug::Pixi ${PIXI_VERSION} removed only the nine samples local-source references. Keeping the committed references; all other lockfile changes remain strict. Remove this temporary exception once the upstream fix is released and verified."
echo "| \`${manifest}\` | Known Pixi samples-source pruning bug: warning only; committed local-source references retained. |" >> "${GITHUB_STEP_SUMMARY}"
git cat-file blob "${candidate_original}" > "${lockfile}"
echo "::endgroup::"
continue
fi
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
Expand Down Expand Up @@ -256,6 +287,19 @@ jobs:
python3 ci/tools/classify_pixi_lockfile_freshness.py "${classifier_args[@]}"
)"

# Do not let a matching solver result hide PR-specific manifest
# changes, even if they happen to produce the same lockfile blobs.
if ((known_samples_pruning != 0)) && \
[[ "${classification}" == "base-maintenance" ]] && \
git diff --quiet "${BASE_SHA}" HEAD -- \
':(glob)**/pixi.toml' ':(glob)**/pyproject.toml' ci/pixi-version.env; then
echo "::warning title=Known inherited Pixi samples lockfile bug::Base and candidate reproduce the same nine-reference samples-source pruning defect, with no PR changes to lock inputs. Keeping the committed local-source references; all other lockfile changes remain strict. Remove this temporary exception once the upstream fix is released and verified."
echo "| \`${manifest}\` | Known inherited Pixi samples-source pruning bug: warning only; no lock-input changes in this PR. Committed local-source references retained. |" >> "${GITHUB_STEP_SUMMARY}"
git cat-file blob "${candidate_original}" > "${lockfile}"
echo "::endgroup::"
continue
fi

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}."
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/ci-pixi-source-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ defaults:
shell: bash --noprofile --norc -xeuo pipefail {0}

env:
# Install from the committed lockfile only. Covers nested `pixi run` calls
# from root pixi.toml tasks; a stale lock must fail rather than being
# refreshed in CI (#2298). Equivalent to `pixi run --locked`.
PIXI_LOCKED: "true"
# Temporarily bypass Pixi's workspace-wide freshness check: the unrelated
# samples environment oscillates because excluded PyPI dependencies are
# still checked. Frozen mode keeps the committed solution and source builds,
# including nested `pixi run` calls. The separate freshness workflow rejects
# every change except the exact known samples-source pruning defect.
# Restore PIXI_LOCKED after the upstream fix is released and verified.
PIXI_FROZEN: "true"

jobs:
# ── PR guard: CPU-only build + import + placement smoke ──
Expand Down
25 changes: 25 additions & 0 deletions ci/README.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a pixi issue to watch?

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ The pinned checkout action's
[`README`](https://github.com/actions/checkout/blob/3d3c42e5aac5ba805825da76410c181273ba90b1/README.md#fetch-all-history-for-all-tags-and-branches)
documents `fetch-depth: 0` for fetching all history, branches, and tags.

## Temporary Pixi Samples Lockfile Workaround

Pixi 0.73.0 checks PyPI dependencies that the `samples` environment explicitly
excludes, then drops its local Conda source references during a PyPI-only
refresh. The next refresh restores them, so repeated lock commands do not
converge. A general maintenance refresh cannot repair this defect.

Source-build CI temporarily uses `PIXI_FROZEN=true`: it builds the checkout's
source packages using the committed dependency solution without triggering
Pixi's workspace-wide freshness check.

The separate freshness workflow remains strict except for one exact pattern:
Pixi 0.73.0 exits 1 and removes precisely the nine `samples` source references,
with every other lockfile byte unchanged. The detector also verifies the
expected manifest overrides, local paths, platforms, and source-record
definitions. It rejects the reverse cycle, which would add missing references.
For PRs, the base must produce identical original and repaired blobs, and no
`pixi.toml`, `pyproject.toml`, or Pixi-version input may differ from the base.
Recognized cases emit a warning and restore the committed references.

Remove the detector and exception, and restore `PIXI_LOCKED=true`, once a
released Pixi passes repeated lock and check commands without changing either
the reduced upstream reproducer or our full workspaces. The refresh workflow
still refuses to open an unstable lockfile-refresh PR.

## Repository Customizations

The workflows in this repository use the `CI_CUSTOMIZATIONS_*` namespace for
Expand Down
139 changes: 139 additions & 0 deletions ci/tools/check_pixi_samples_source_pruning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Recognize one temporary Pixi 0.73.0 source-pruning defect, failing closed."""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

import tomllib

LOCAL_PACKAGES = {
"cuda-bindings": "../cuda_bindings",
"cuda-core": ".",
"cuda-pathfinder": "../cuda_pathfinder",
}
PLATFORMS = {"p1": "linux-64", "p2": "linux-aarch64", "p3": "win-64"}
SOURCE_REFERENCE = re.compile(
r" - conda_source: (cuda-bindings|cuda-core|cuda-pathfinder)\[([0-9a-f]{8})\] @ (\S+)\n"
)


def is_known_samples_source_pruning(original: str, repaired: str) -> bool:
"""Accept only removal of the nine known samples references, with nothing else changed.

This deliberately understands only the pinned version's exact serialization.
Source record definitions remain checked byte-for-byte along with the rest of
the lockfile; missing definitions and the reverse (addition) cycle are rejected.
"""
lines = original.splitlines(keepends=True)
if not lines or lines[0] != "version: 7\n" or lines.count("environments:\n") != 1:
return False
for alias, platform in PLATFORMS.items():
if original.count(f"- name: {alias}\n subdir: {platform}\n") != 1:
return False

environment_start = lines.index("environments:\n") + 1
environment_end = next(
(index for index in range(environment_start, len(lines)) if not lines[index].startswith(" ")),
len(lines),
)
sample_headers = [index for index in range(environment_start, environment_end) if lines[index] == " samples:\n"]
if len(sample_headers) != 1:
return False
samples_start = sample_headers[0] + 1
samples_end = next(
(index for index in range(samples_start, environment_end) if not lines[index].startswith(" ")),
environment_end,
)
if any(lines[samples_start:samples_end].count(f" {alias}:\n") != 1 for alias in PLATFORMS):
return False

platform = None
seen: set[tuple[str, str]] = set()
removed: set[int] = set()
for index in range(samples_start, samples_end):
line = lines[index]
platform_match = re.fullmatch(r" ([^\s:]+):\n", line)
if platform_match:
platform = platform_match[1] if platform_match[1] in PLATFORMS else None
reference = SOURCE_REFERENCE.fullmatch(line)
if reference is None:
continue
name, identifier, path = reference.groups()
if platform is None or path != LOCAL_PACKAGES[name] or (platform, name) in seen:
return False
definition = f"- conda_source: {name}[{identifier}] @ {path}\n"
if lines[environment_end:].count(definition) != 1:
return False
seen.add((platform, name))
removed.add(index)

expected = {(platform, name) for platform in PLATFORMS for name in LOCAL_PACKAGES}
return seen == expected and "".join(line for index, line in enumerate(lines) if index not in removed) == repaired


def _manifest_matches(manifest: dict) -> bool:
features = manifest.get("feature", {})
if not isinstance(features, dict):
return False
samples = features.get("samples", {})
local_deps = features.get("local-deps", {})
environments = manifest.get("environments", {})
if not all(isinstance(table, dict) for table in (samples, local_deps, environments)):
return False
options = samples.get("pypi-options", {})
sample_dependencies = samples.get("dependencies", {})
local_dependencies = local_deps.get("dependencies", {})
if not all(isinstance(table, dict) for table in (options, sample_dependencies, local_dependencies)):
return False
overrides = options.get("dependency-overrides", {})
if overrides != {name: {"version": "*", "env-markers": "python_version < '0'"} for name in LOCAL_PACKAGES}:
return False
if sample_dependencies.get("cuda-core") != {"path": "."}:
return False
if any(
local_dependencies.get(name) != {"path": path} for name, path in LOCAL_PACKAGES.items() if name != "cuda-core"
):
return False
environment = environments.get("samples", {})
return isinstance(environment, dict) and environment.get("features") == ["cu13", "samples", "local-deps"]


def main(argv: list[str] | None = None) -> int:
"""Exit 0 for the known defect, 1 otherwise, and 2 for an operational error."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--original-blob", required=True)
parser.add_argument("--lockfile", type=Path, required=True)
parser.add_argument("--manifest-path", type=Path, required=True)
parser.add_argument("--pixi-version", required=True)
parser.add_argument("--check-status", type=int, required=True)
args = parser.parse_args(argv)
if args.pixi_version.removeprefix("v") != "0.73.0" or args.check_status != 1:
return 1
if re.fullmatch(r"[0-9a-f]{40}", args.original_blob) is None:
print("Source-pruning check: original blob must be a full Git SHA-1.", file=sys.stderr)
return 2
try:
original = subprocess.run( # noqa: S603 - validated SHA, argv passed without a shell.
["git", "cat-file", "blob", args.original_blob], # noqa: S607
check=True,
capture_output=True,
).stdout.decode("utf-8")
repaired = args.lockfile.read_bytes().decode("utf-8")
manifest = tomllib.loads(args.manifest_path.read_bytes().decode("utf-8"))
except (OSError, UnicodeError, tomllib.TOMLDecodeError, subprocess.CalledProcessError) as error:
print(f"Source-pruning check could not read its inputs: {error}", file=sys.stderr)
return 2
if not _manifest_matches(manifest):
return 1
return 0 if is_known_samples_source_pruning(original, repaired) else 1


if __name__ == "__main__":
sys.exit(main())
Loading
Loading