Skip to content

Commit f6387f0

Browse files
committed
ci: work around Pixi samples-source lockfile oscillation
1 parent a2b1e31 commit f6387f0

5 files changed

Lines changed: 442 additions & 6 deletions

File tree

‎.github/workflows/ci-pixi-lockfile-freshness-check.yml‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
# its bytes are not what the pinned pixi version generates. Pull requests also
77
# check stale workspaces at the base SHA so diagnostics can distinguish changes
88
# introduced by the PR from maintenance already needed on the base branch.
9-
# Attribution changes remediation only; the aggregate job remains strict for
10-
# every stale classification. See #2298 and #2926.
9+
# Attribution changes remediation only. A temporary, exact-match exception
10+
# covers Pixi 0.73.0 pruning the samples environment's local source references;
11+
# every other stale classification remains strict. See #2298 and #2926.
1112

1213
name: "CI: pixi lockfile freshness check"
1314

@@ -28,6 +29,8 @@ on:
2829
- "**/pixi.lock"
2930
- "**/pyproject.toml"
3031
- "ci/tools/classify_pixi_lockfile_freshness.py"
32+
- "ci/tools/check_pixi_samples_source_pruning.py"
33+
- "ci/tools/tests/test_check_pixi_samples_source_pruning.py"
3134
- "ci/tools/list_pixi_workspaces.py"
3235
- "ci/pixi-version.env"
3336
- ".github/actions/setup-pixi/action.yml"
@@ -195,7 +198,35 @@ jobs:
195198
continue
196199
fi
197200
201+
known_samples_pruning=0
202+
if [[ "${manifest}" == "cuda_core" ]]; then
203+
defect_status=0
204+
python3 ci/tools/check_pixi_samples_source_pruning.py \
205+
--original-blob "${candidate_original}" \
206+
--lockfile "${lockfile}" \
207+
--manifest-path "${manifest}/pixi.toml" \
208+
--pixi-version "${PIXI_VERSION}" \
209+
--check-status "${check_status}" \
210+
|| defect_status=$?
211+
if ((defect_status == 0)); then
212+
known_samples_pruning=1
213+
elif ((defect_status != 1)); then
214+
echo "::error::Could not determine whether '${lockfile}' matches the known Pixi samples-source pruning defect. Resolve the detector error above and rerun."
215+
echo "| \`${manifest}\` | Operational error: known-defect detector failed. |" >> "${GITHUB_STEP_SUMMARY}"
216+
failed=1
217+
echo "::endgroup::"
218+
continue
219+
fi
220+
fi
221+
198222
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
223+
if ((known_samples_pruning != 0)); then
224+
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."
225+
echo "| \`${manifest}\` | Known Pixi samples-source pruning bug: warning only; committed local-source references retained. |" >> "${GITHUB_STEP_SUMMARY}"
226+
git cat-file blob "${candidate_original}" > "${lockfile}"
227+
echo "::endgroup::"
228+
continue
229+
fi
199230
echo "::error title=Stale Pixi lockfile::Lockfile '${lockfile}' is stale. Regenerate and commit it with pixi ${PIXI_VERSION}: pixi lock --manifest-path ${manifest}."
200231
echo "| \`${manifest}\` | Stale: regenerate and commit the lockfile. |" >> "${GITHUB_STEP_SUMMARY}"
201232
failed=1
@@ -256,6 +287,19 @@ jobs:
256287
python3 ci/tools/classify_pixi_lockfile_freshness.py "${classifier_args[@]}"
257288
)"
258289
290+
# Do not let a matching solver result hide PR-specific manifest
291+
# changes, even if they happen to produce the same lockfile blobs.
292+
if ((known_samples_pruning != 0)) && \
293+
[[ "${classification}" == "base-maintenance" ]] && \
294+
git diff --quiet "${BASE_SHA}" HEAD -- \
295+
':(glob)**/pixi.toml' ':(glob)**/pyproject.toml' ci/pixi-version.env; then
296+
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."
297+
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}"
298+
git cat-file blob "${candidate_original}" > "${lockfile}"
299+
echo "::endgroup::"
300+
continue
301+
fi
302+
259303
case "${classification}" in
260304
pr-induced)
261305
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}."

‎.github/workflows/ci-pixi-source-test.yml‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ defaults:
6262
shell: bash --noprofile --norc -xeuo pipefail {0}
6363

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

7073
jobs:
7174
# ── PR guard: CPU-only build + import + placement smoke ──

‎ci/README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ The pinned checkout action's
2525
[`README`](https://github.com/actions/checkout/blob/3d3c42e5aac5ba805825da76410c181273ba90b1/README.md#fetch-all-history-for-all-tags-and-branches)
2626
documents `fetch-depth: 0` for fetching all history, branches, and tags.
2727

28+
## Temporary Pixi Samples Lockfile Workaround
29+
30+
Pixi 0.73.0 checks PyPI dependencies that the `samples` environment explicitly
31+
excludes, then drops its local Conda source references during a PyPI-only
32+
refresh. The next refresh restores them, so repeated lock commands do not
33+
converge. A general maintenance refresh cannot repair this defect.
34+
35+
Source-build CI temporarily uses `PIXI_FROZEN=true`: it builds the checkout's
36+
source packages using the committed dependency solution without triggering
37+
Pixi's workspace-wide freshness check.
38+
39+
The separate freshness workflow remains strict except for one exact pattern:
40+
Pixi 0.73.0 exits 1 and removes precisely the nine `samples` source references,
41+
with every other lockfile byte unchanged. The detector also verifies the
42+
expected manifest overrides, local paths, platforms, and source-record
43+
definitions. It rejects the reverse cycle, which would add missing references.
44+
For PRs, the base must produce identical original and repaired blobs, and no
45+
`pixi.toml`, `pyproject.toml`, or Pixi-version input may differ from the base.
46+
Recognized cases emit a warning and restore the committed references.
47+
48+
Remove the detector and exception, and restore `PIXI_LOCKED=true`, once a
49+
released Pixi passes repeated lock and check commands without changing either
50+
the reduced upstream reproducer or our full workspaces. The refresh workflow
51+
still refuses to open an unstable lockfile-refresh PR.
52+
2853
## Repository Customizations
2954

3055
The workflows in this repository use the `CI_CUSTOMIZATIONS_*` namespace for
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Recognize one temporary Pixi 0.73.0 source-pruning defect, failing closed."""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import re
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
14+
import tomllib
15+
16+
LOCAL_PACKAGES = {
17+
"cuda-bindings": "../cuda_bindings",
18+
"cuda-core": ".",
19+
"cuda-pathfinder": "../cuda_pathfinder",
20+
}
21+
PLATFORMS = {"p1": "linux-64", "p2": "linux-aarch64", "p3": "win-64"}
22+
SOURCE_REFERENCE = re.compile(
23+
r" - conda_source: (cuda-bindings|cuda-core|cuda-pathfinder)\[([0-9a-f]{8})\] @ (\S+)\n"
24+
)
25+
26+
27+
def is_known_samples_source_pruning(original: str, repaired: str) -> bool:
28+
"""Accept only removal of the nine known samples references, with nothing else changed.
29+
30+
This deliberately understands only the pinned version's exact serialization.
31+
Source record definitions remain checked byte-for-byte along with the rest of
32+
the lockfile; missing definitions and the reverse (addition) cycle are rejected.
33+
"""
34+
lines = original.splitlines(keepends=True)
35+
if not lines or lines[0] != "version: 7\n" or lines.count("environments:\n") != 1:
36+
return False
37+
for alias, platform in PLATFORMS.items():
38+
if original.count(f"- name: {alias}\n subdir: {platform}\n") != 1:
39+
return False
40+
41+
environment_start = lines.index("environments:\n") + 1
42+
environment_end = next(
43+
(index for index in range(environment_start, len(lines)) if not lines[index].startswith(" ")),
44+
len(lines),
45+
)
46+
sample_headers = [index for index in range(environment_start, environment_end) if lines[index] == " samples:\n"]
47+
if len(sample_headers) != 1:
48+
return False
49+
samples_start = sample_headers[0] + 1
50+
samples_end = next(
51+
(index for index in range(samples_start, environment_end) if not lines[index].startswith(" ")),
52+
environment_end,
53+
)
54+
if any(lines[samples_start:samples_end].count(f" {alias}:\n") != 1 for alias in PLATFORMS):
55+
return False
56+
57+
platform = None
58+
seen: set[tuple[str, str]] = set()
59+
removed: set[int] = set()
60+
for index in range(samples_start, samples_end):
61+
line = lines[index]
62+
platform_match = re.fullmatch(r" ([^\s:]+):\n", line)
63+
if platform_match:
64+
platform = platform_match[1] if platform_match[1] in PLATFORMS else None
65+
reference = SOURCE_REFERENCE.fullmatch(line)
66+
if reference is None:
67+
continue
68+
name, identifier, path = reference.groups()
69+
if platform is None or path != LOCAL_PACKAGES[name] or (platform, name) in seen:
70+
return False
71+
definition = f"- conda_source: {name}[{identifier}] @ {path}\n"
72+
if lines[environment_end:].count(definition) != 1:
73+
return False
74+
seen.add((platform, name))
75+
removed.add(index)
76+
77+
expected = {(platform, name) for platform in PLATFORMS for name in LOCAL_PACKAGES}
78+
return seen == expected and "".join(line for index, line in enumerate(lines) if index not in removed) == repaired
79+
80+
81+
def _manifest_matches(manifest: dict) -> bool:
82+
features = manifest.get("feature", {})
83+
if not isinstance(features, dict):
84+
return False
85+
samples = features.get("samples", {})
86+
local_deps = features.get("local-deps", {})
87+
environments = manifest.get("environments", {})
88+
if not all(isinstance(table, dict) for table in (samples, local_deps, environments)):
89+
return False
90+
options = samples.get("pypi-options", {})
91+
sample_dependencies = samples.get("dependencies", {})
92+
local_dependencies = local_deps.get("dependencies", {})
93+
if not all(isinstance(table, dict) for table in (options, sample_dependencies, local_dependencies)):
94+
return False
95+
overrides = options.get("dependency-overrides", {})
96+
if overrides != {name: {"version": "*", "env-markers": "python_version < '0'"} for name in LOCAL_PACKAGES}:
97+
return False
98+
if sample_dependencies.get("cuda-core") != {"path": "."}:
99+
return False
100+
if any(
101+
local_dependencies.get(name) != {"path": path} for name, path in LOCAL_PACKAGES.items() if name != "cuda-core"
102+
):
103+
return False
104+
environment = environments.get("samples", {})
105+
return isinstance(environment, dict) and environment.get("features") == ["cu13", "samples", "local-deps"]
106+
107+
108+
def main(argv: list[str] | None = None) -> int:
109+
"""Exit 0 for the known defect, 1 otherwise, and 2 for an operational error."""
110+
parser = argparse.ArgumentParser(description=__doc__)
111+
parser.add_argument("--original-blob", required=True)
112+
parser.add_argument("--lockfile", type=Path, required=True)
113+
parser.add_argument("--manifest-path", type=Path, required=True)
114+
parser.add_argument("--pixi-version", required=True)
115+
parser.add_argument("--check-status", type=int, required=True)
116+
args = parser.parse_args(argv)
117+
if args.pixi_version.removeprefix("v") != "0.73.0" or args.check_status != 1:
118+
return 1
119+
if re.fullmatch(r"[0-9a-f]{40}", args.original_blob) is None:
120+
print("Source-pruning check: original blob must be a full Git SHA-1.", file=sys.stderr)
121+
return 2
122+
try:
123+
original = subprocess.run( # noqa: S603 - validated SHA, argv passed without a shell.
124+
["git", "cat-file", "blob", args.original_blob], # noqa: S607
125+
check=True,
126+
capture_output=True,
127+
).stdout.decode("utf-8")
128+
repaired = args.lockfile.read_bytes().decode("utf-8")
129+
manifest = tomllib.loads(args.manifest_path.read_bytes().decode("utf-8"))
130+
except (OSError, UnicodeError, tomllib.TOMLDecodeError, subprocess.CalledProcessError) as error:
131+
print(f"Source-pruning check could not read its inputs: {error}", file=sys.stderr)
132+
return 2
133+
if not _manifest_matches(manifest):
134+
return 1
135+
return 0 if is_known_samples_source_pruning(original, repaired) else 1
136+
137+
138+
if __name__ == "__main__":
139+
sys.exit(main())

0 commit comments

Comments
 (0)