fix(ci): align uv pins across CI and release workflows (#1016)

This commit is contained in:
Sergey Silaev
2026-10-24 22:23:51 +05:30
committed by GitHub
parent b59797f1af
commit 2b2291ce69
4 changed files with 70 additions and 17 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
- name: Set up pinned uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: '0.12.23'
version: '0.13.3'
- name: Extract version from tag
id: get_version
+1 -1
View File
@@ -113,7 +113,7 @@ jobs:
- name: Set up pinned uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: '0.12.23'
version: '0.13.3'
- name: Build and verify reproducible wheel
run: python scripts/build_reproducible_wheel.py --python "$(command -v python)"
- name: Preserve failed wheel builds and diagnostics
+65 -13
View File
@@ -16,11 +16,16 @@ from pathlib import Path
from types import ModuleType
from typing import Any
import pytest
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
JUSTFILE = REPO_ROOT / "justfile"
INSTALLER = REPO_ROOT / "install.sh"
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "unified-pipeline.yml"
RELEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "release.yml"
UV_EXPORT_VERSION = "0.13.3"
REQUIREMENTS = REPO_ROOT / "requirements"
DEV_EXPORT = REQUIREMENTS / "dev.txt"
@@ -39,25 +44,72 @@ def _pyproject() -> dict[str, Any]:
return tomllib.load(handle)
def test_ci_uv_pin_is_supported_and_consistent() -> None:
"""All CI jobs must use one supported uv to reproduce the committed exports."""
def _assert_workflow_uv_pins(text: str) -> None:
"""Check action inputs, not display names or YAML quoting conventions."""
jobs = yaml.safe_load(text)["jobs"]
checked = []
for name, job in jobs.items():
for step in job.get("steps", []):
if not step.get("uses", "").startswith("astral-sh/setup-uv@"):
continue
pin = step.get("with", {}).get("version")
assert pin == UV_EXPORT_VERSION, (
f"{name}: setup-uv must pin the export generation version "
f"{UV_EXPORT_VERSION}, got {pin!r}"
)
checked.append(name)
assert checked, "no setup-uv steps checked"
@pytest.mark.parametrize("workflow", [WORKFLOW, RELEASE_WORKFLOW], ids=lambda path: path.name)
def test_ci_uv_pin_is_supported_and_consistent(workflow: Path) -> None:
"""PR and release jobs must reproduce exports with the same supported uv."""
required = _pyproject()["tool"]["uv"]["required-version"]
bounds = re.fullmatch(r">=(\d+\.\d+),<(\d+\.\d+)", required)
assert bounds, f"unexpected uv required-version: {required!r}"
workflow = WORKFLOW.read_text(encoding="utf-8")
steps = re.findall(r"- name: Set up uv\n(.*?)(?=\n\s+- name:|\Z)", workflow, re.DOTALL)
assert steps and len(steps) == workflow.count("uses: astral-sh/setup-uv@")
pins = []
for step in steps:
pin = re.search(r'^\s+version: "(\d+\.\d+\.\d+)"$', step, re.MULTILINE)
assert pin, "setup-uv in CI must explicitly pin the export generation version"
pins.append(pin.group(1))
assert len(set(pins)) == 1, "uv versions in CI disagree; exports are byte-for-byte checked"
minimum, maximum = (tuple(map(int, bound.split("."))) for bound in bounds.groups())
minor = tuple(map(int, pins[0].split(".")[:2]))
minor = tuple(map(int, UV_EXPORT_VERSION.split(".")[:2]))
assert minimum <= minor < maximum
_assert_workflow_uv_pins(workflow.read_text(encoding="utf-8"))
@pytest.mark.parametrize("name", ["Set up uv", "Set up pinned uv", "Install build tooling", None])
def test_uv_pin_check_does_not_depend_on_step_name(name: str | None) -> None:
step = {"uses": "astral-sh/setup-uv@commit", "with": {"version": UV_EXPORT_VERSION}}
if name is not None:
step["name"] = name
workflow = {"jobs": {"build": {"steps": [step]}, "reusable": {"uses": "./other.yml"}}}
_assert_workflow_uv_pins(yaml.safe_dump(workflow))
@pytest.mark.parametrize("pin", ["0.12.23", "0.13.2", "latest", None])
def test_uv_pin_check_rejects_wrong_or_missing_pin_in_any_job(pin: str | None) -> None:
workflow = {
"jobs": {
"lint": {
"steps": [
{"uses": "astral-sh/setup-uv@commit", "with": {"version": UV_EXPORT_VERSION}}
]
},
"wheel": {
"steps": [
{
"name": "Set up pinned uv",
"uses": "astral-sh/setup-uv@commit",
"with": {"version": pin} if pin is not None else {},
}
]
},
}
}
with pytest.raises(AssertionError, match="wheel: setup-uv must pin"):
_assert_workflow_uv_pins(yaml.safe_dump(workflow))
def test_uv_pin_check_requires_at_least_one_setup_step() -> None:
with pytest.raises(AssertionError, match="no setup-uv steps checked"):
_assert_workflow_uv_pins("jobs:\n build:\n steps:\n - run: uv --version\n")
def _requirement_names(specs: list[str]) -> list[str]:
+3 -2
View File
@@ -93,7 +93,8 @@ def test_only_verified_canonical_distributions_are_published():
assert "python -m build" not in _without_comments(_text())
assert "pip install" not in _without_comments(block.split(" - name: Create Release")[0])
assert "pip install" not in _without_comments(_jobs()["publish-pypi"])
assert "version: '0.12.23'" in block
# Exact uv versions are checked across both workflows in test_dependency_exports.py.
assert "astral-sh/setup-uv@" in block
assert "dist/*.whl" in block and "dist/*.tar.gz" in block
assert "path: build/reproducible-wheel/" in block
@@ -104,7 +105,7 @@ def test_pr_ci_runs_the_same_reproducibility_gate():
block = ci.split("\n reproducible-wheel:\n", 1)[1].split("\n python-lint:", 1)[0]
assert "python scripts/build_reproducible_wheel.py" in block
assert "python-version: '3.11'" in block
assert "version: '0.12.23'" in block
assert "astral-sh/setup-uv@" in block
assert not re.search(r"^ if:", block, re.M)