From 2b2291ce692addf7a3374ad666bc89bb742416bc Mon Sep 17 00:00:00 2001 From: Sergey Silaev Date: Sat, 24 Oct 2026 18:53:51 +0200 Subject: [PATCH] fix(ci): align uv pins across CI and release workflows (#1016) --- .github/workflows/release.yml | 2 +- .github/workflows/unified-pipeline.yml | 2 +- tests/test_dependency_exports.py | 78 +++++++++++++++++++++----- tests/test_release_integrity.py | 5 +- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4d8473..a436e22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/unified-pipeline.yml b/.github/workflows/unified-pipeline.yml index d5834df..ec47b6c 100644 --- a/.github/workflows/unified-pipeline.yml +++ b/.github/workflows/unified-pipeline.yml @@ -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 diff --git a/tests/test_dependency_exports.py b/tests/test_dependency_exports.py index 42b16cf..806fa70 100644 --- a/tests/test_dependency_exports.py +++ b/tests/test_dependency_exports.py @@ -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]: diff --git a/tests/test_release_integrity.py b/tests/test_release_integrity.py index 7ff3768..41c4870 100644 --- a/tests/test_release_integrity.py +++ b/tests/test_release_integrity.py @@ -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)