From 775f80d1368ee842daca0033488478eb0c2867ad Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 21 Oct 2026 11:56:35 -0500 Subject: [PATCH] ci: lock and audit the uv bootstrap executable --- .github/dependabot.yml | 2 +- .github/pull_request_template.md | 17 ++- .github/workflows/release.yml | 2 +- .github/workflows/security-scan.yml | 4 +- .github/workflows/validate.yml | 6 +- CONTRIBUTING.md | 19 ++- docs/dependency-release-impact.md | 74 +++++++++++ pyproject.toml | 10 +- scripts/audit-dependencies.sh | 5 + scripts/bootstrap_uv.py | 67 ++++++++++ tests/test_bootstrap_uv.py | 170 ++++++++++++++++++++++++ tests/test_dependency_audit_workflow.py | 34 ++++- uv.lock | 32 ++++- 13 files changed, 425 insertions(+), 17 deletions(-) create mode 100644 docs/dependency-release-impact.md create mode 100644 scripts/bootstrap_uv.py create mode 100644 tests/test_bootstrap_uv.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf8b3cf..97e8bfe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ # Auto-merge (dependabot-auto-merge.yml) supplies the ruleset's guarded bot approval # and squash-merges after Validate is green. Human-authored PRs still require human # approval. auto-fix.yml mechanically repairs lint-only failures. -# Non-releasing commit prefixes (chore/ci) so a merged bump never cuts a release. +# chore/ci do not request a bump themselves; accumulated history can still release. version: 2 updates: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 07770b3..e426469 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,7 +12,7 @@ Commit title, e.g.: - `feat: add X` (minor release) - `fix: correct Y` (patch release) -- `ci:` / `chore:` / `docs:` / `refactor:` / `test:` (no release) +- `ci:` / `chore:` / `docs:` / `refactor:` / `test:` (no bump by themselves) ## Checklist @@ -20,3 +20,18 @@ Commit title, e.g.: - [ ] The local gate passes: `uv run prek run --all-files` (see `AGENTS.md`). - [ ] No changes to `src/skill_optimizer/**`, `optimize_description_v2.py`, or `tests/**` unless intended. + +## Dependency release impact + +For dependency, bootstrap or packaging updates, complete the +[release-impact check](../docs/dependency-release-impact.md): + +- Changed dependency and advisory/fixed-version evidence: +- Affected surface: runtime/bundled, source build, or CI/development/bootstrap: +- Published version/tag and actual wheel/source-archive evidence compared: +- Candidate build/install evidence: +- Accumulated commits and read-only semantic-release decision: +- Release decision: no distribution needed / new distribution needed / blocked: +- If a release would be triggered by merge, approval and expected contents: + +For other changes, state why this section is not applicable. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a392018..f25b848 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock # No cache in the publish path — avoids a cache-poisoning vector where a # restored cache could influence the artifact that gets published. enable-cache: false diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ea88348..b66d63b 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -37,7 +37,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock - name: Sync the environment env: @@ -68,7 +68,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock # gate owns the shared cache write; parallel saves race on the same key. save-cache: false diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ef769c5..67d74b7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -39,7 +39,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock # --locked asserts uv.lock is already in sync with pyproject.toml and fails # otherwise, so a dependency change that skips `uv lock` cannot merge. The gate @@ -74,7 +74,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock # gate owns the shared cache write; parallel saves race on the same key. save-cache: false @@ -179,7 +179,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.11.29" + version-file: uv.lock - name: Compute mechanical fixes as self-contained data id: patch diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b4785d..d4817c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,8 +21,9 @@ so `uv sync` installs it). Install the git hooks once with `prek install -f`. - **Conventional Commits.** PRs are squash-merged, so the **PR title becomes the commit message** that drives releases. Use `feat:` (minor), `fix:` (patch), or a - non-releasing type (`ci:`, `chore:`, `docs:`, `refactor:`, `test:`). The `pr-title` - check enforces this. + type that does not itself request a bump (`ci:`, `chore:`, `docs:`, `refactor:`, + `test:`). Earlier unreleased changes can still trigger a release. The `pr-title` + check enforces the title syntax. - **Keep the tool code stable.** `src/skill_optimizer/**`, `optimize_description_v2.py`, and `tests/**` are the finished tool; change them only with intent. @@ -123,3 +124,17 @@ bash scripts/audit-dependencies.sh Reports are written under `${RUNNER_TEMP:-${TMPDIR:-/tmp}}/optimizer-dependency-audit`. Use the raw reports to investigate a failure; never change a scanner exit code or weaken a required check to make an update merge. + +## Dependency changes and published artifacts + +Complete the [dependency release-impact check](docs/dependency-release-impact.md) +for dependency, bootstrap and packaging PRs. Green checks on main do not prove that +an already published source archive or wheel contains an update. Record whether a +new distribution is needed and what the existing main-push release workflow would +do before merging. + +The uv executable has one exact, Dependabot-owned requirement in the non-default +`bootstrap` group. Its `uv.lock` entry supplies every setup-uv step and the semantic +release build hook. Both scanners cover it, and the audit refuses to run with a +different installed uv version. Update the requirement and lock together, without +adding a separate workflow or hook version override. diff --git a/docs/dependency-release-impact.md b/docs/dependency-release-impact.md new file mode 100644 index 0000000..cce66a7 --- /dev/null +++ b/docs/dependency-release-impact.md @@ -0,0 +1,74 @@ +# Dependency release-impact check + +A green dependency scan on `main` is not proof that published artifacts contain a +change. Classify every dependency PR before merge and record its release decision +in the PR. The pull-request template provides the checklist. Do not change release +permissions, publish flags, or security exceptions to make a check pass. + +## What needs a release? + +- **Runtime or bundled dependency:** inspect the published wheel's `METADATA` + (`Requires-Dist`) and archive contents. If affected code or a dependency + requirement ships to users, a new distribution is needed to deliver the fix. +- **Build backend or source-install requirement:** inspect the published source + archive's `pyproject.toml`. A changed build-system range reaches source-install + users only in a new source distribution. This is distinct from a vulnerability + in the installed runtime. +- **CI, development, or release bootstrap only:** a workflow/lock update protects + future development and build runs after merge. A new runtime release is not + needed solely to change the maintainer's executable. Confirm that it is not a + runtime dependency or bundled into the wheel. +- **Mixed change:** document each surface separately. Do not infer release impact + from a package name, dependency group, or the PR title alone. + +## Evidence to record before merge + +1. Record the exact PR head SHA, current `main` SHA, latest release tag and its + commit, and the published version/files being compared. Compare all commits + since that release, including their bodies and breaking-change markers. +2. Record exact old/new dependency versions, advisory/fixed-version evidence, and + the lockfile inventory used by the unsuppressed scanners. For uv, verify the + installed executable against the lock with + `uv run --locked python -m scripts.bootstrap_uv --installed-version "$(uv --version)"`. +3. Inspect the actual published wheel and source archive. Record runtime + requirements, build requirements, and any bundled changed code. If published + files are unavailable, mark this comparison blocked rather than infer parity + from repository metadata. +4. Build a candidate with `uv build --no-sources`, inspect its wheel `METADATA` and + source `pyproject.toml`, and exercise the documented install/entry-point checks. + A local build is verification, not publication. Follow `AGENTS.md`'s live-CLI + release gates before approving a release. +5. Check the pinned python-semantic-release version's **read-only** + `semantic-release version --print` result against the proposed history. Also + inspect the release workflow's triggers and publish condition. `ci:` and + `chore:` do not themselves request a bump, but an earlier unreleased `fix:` or + `feat:` can cause this merge to release accumulated changes. +6. State one decision: **no distribution needed**, **new distribution needed**, or + **blocked pending evidence/approval**. State what a release would contain and + whether the existing main-push workflow will create one. If publication needs + approval, obtain it before merging a change that would trigger that publication. +7. After an approved merge, verify checks on the exact main SHA and inspect the + Release run. If a release was approved, verify its tag/commit, published file + hashes and installed version. A tag or green validation alone does not prove + that PyPI accepted the expected wheel and source archive. + +## Bootstrap remediation: 2026-10-21 + +[GHSA-hq85-5cpj-mr62](https://github.com/astral-sh/uv/security/advisories/GHSA-hq85-5cpj-mr62) +affects uv versions before 0.13.4. The repository's hardcoded 0.11.29 executable +was outside `uv.lock`, so lock-only vulnerability scans did not cover it. + +The fix places `uv==0.13.4` in the non-default `bootstrap` dependency group and its +exact PyPI package in `uv.lock`. Every setup-uv step reads that lockfile; the +semantic-release hook reads and validates the same requirement. The audit verifies +the running executable before both scanners, and their existing full-inventory +checks now include uv. Missing pins, drift, incomplete scans and scanner errors +fail closed. The advisory-exception policy remains unchanged and empty. + +This repairs build/CI tooling, not a bundled runtime dependency. The tool has no +third-party runtime requirements. Separately, the build backend range on main was +widened from `uv_build>=0.11.28,<0.13` in release v0.2.6 to +`uv_build>=0.11.28,<0.14`; source-install users need a later source distribution to +receive that compatibility change. A new runtime vulnerability was not established +by that range change. Record the final release decision and artifact evidence in +the remediation PR rather than assuming this documentation publishes anything. diff --git a/pyproject.toml b/pyproject.toml index c115382..636fcfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ module-name = "skill_optimizer" module-root = "src" [dependency-groups] +# Dependabot owns this exact bootstrap pin; setup-uv and semantic release read +# the matching uv.lock entry. Keep bootstrap out of the default dev environment. +bootstrap = ["uv==0.13.4"] dev = [ "prek>=0.4.10", "pydoclint>=0.9.1", @@ -169,12 +172,15 @@ extraPaths = ["src"] # GitHub Release. Building/publishing to PyPI is handled by a separate job in # .github/workflows/release.yml (uv build + gh-action-pypi-publish). The build hook # only keeps uv.lock synchronized with PSR's version stamp before the release commit. -# The uv pin must match the release workflow. See CONTRIBUTING.md and AGENTS.md. +# The bootstrap pin is owned by the group above and resolved from uv.lock. +# See CONTRIBUTING.md and AGENTS.md. [tool.semantic_release] version_toml = ["pyproject.toml:project.version"] build_command = """ set -eu -python -m pip install --disable-pip-version-check "uv==0.11.29" +UV_REQUIREMENT="$(python -m scripts.bootstrap_uv)" +python -m pip install --disable-pip-version-check "$UV_REQUIREMENT" +python -m scripts.bootstrap_uv --installed-version "$(uv --version)" uv lock --upgrade-package "$PACKAGE_NAME" git add uv.lock """ diff --git a/scripts/audit-dependencies.sh b/scripts/audit-dependencies.sh index 370c96b..d27a1f7 100644 --- a/scripts/audit-dependencies.sh +++ b/scripts/audit-dependencies.sh @@ -4,6 +4,11 @@ set -euo pipefail output_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/optimizer-dependency-audit" mkdir -p "$output_dir" +# Prove that the executable being used is the exact package included in both +# scanners' lock inventory. Missing or divergent bootstrap state fails closed. +uv run --locked python -m scripts.bootstrap_uv --installed-version "$(uv --version)" \ + | tee "$output_dir/bootstrap.txt" + summary_args=() if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then summary_args=(--summary "$GITHUB_STEP_SUMMARY") diff --git a/scripts/bootstrap_uv.py b/scripts/bootstrap_uv.py new file mode 100644 index 0000000..fc585c5 --- /dev/null +++ b/scripts/bootstrap_uv.py @@ -0,0 +1,67 @@ +"""Bind the installed uv bootstrap executable to the audited lockfile package.""" + +import argparse +import re +import sys +import tomllib +from collections.abc import Sequence +from pathlib import Path + +from scripts.advisory_policy import PolicyError, array, mapping, require, string + + +def bootstrap_requirement(project: object, lock: object) -> str: + """Return the sole exact bootstrap requirement, rejecting divergent lock state.""" + groups = mapping(mapping(project, "project").get("dependency-groups"), "groups") + requirements = array(groups.get("bootstrap"), "bootstrap group") + require(len(requirements) == 1, "bootstrap must contain exactly one uv requirement") + requirement = string(requirements[0], "bootstrap requirement") + require( + re.fullmatch(r"uv==[0-9]+\.[0-9]+\.[0-9]+", requirement) is not None, + "bootstrap must pin uv==X.Y.Z exactly", + ) + packages = [ + mapping(raw, "locked package") + for raw in array(mapping(lock, "lock").get("package"), "lock.package") + ] + matches = [package for package in packages if package.get("name") == "uv"] + require(len(matches) == 1, "uv.lock must contain exactly one uv package") + package = matches[0] + require( + package.get("source") == {"registry": "https://pypi.org/simple"}, + "bootstrap must be a PyPI registry package in the scanner inventory", + ) + require( + requirement == f"uv=={string(package.get('version'), 'uv version')}", + "bootstrap requirement and uv.lock version disagree", + ) + return requirement + + +def main(argv: Sequence[str] | None = None) -> int: + """Print the locked pip requirement and optionally verify the running executable.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, default=Path("pyproject.toml")) + parser.add_argument("--lock", type=Path, default=Path("uv.lock")) + parser.add_argument("--installed-version") + args = parser.parse_args(argv) + try: + requirement = bootstrap_requirement( + tomllib.loads(args.project.read_text(encoding="utf-8")), + tomllib.loads(args.lock.read_text(encoding="utf-8")), + ) + if args.installed_version is not None: + require( + args.installed_version.split()[:2] + == ["uv", requirement.removeprefix("uv==")], + "installed uv version differs from the audited bootstrap pin", + ) + print(requirement) + except (PolicyError, ValueError, OSError) as error: + print(f"uv bootstrap check failed: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_bootstrap_uv.py b/tests/test_bootstrap_uv.py new file mode 100644 index 0000000..f73a5a4 --- /dev/null +++ b/tests/test_bootstrap_uv.py @@ -0,0 +1,170 @@ +"""Prevent bootstrap pin drift and prove the scanner inventories include uv.""" + +import copy +import re +import tomllib +from pathlib import Path +from typing import Any + +import pytest + +from scripts.advisory_policy import PolicyError, locked_packages, validate_report +from scripts.bootstrap_uv import bootstrap_requirement, main +from scripts.uv_advisory_policy import validate_uv_report + +ROOT = Path(__file__).resolve().parent.parent +PROJECT: dict[str, Any] = {"dependency-groups": {"bootstrap": ["uv==0.13.4"]}} +LOCK: dict[str, Any] = { + "package": [ + { + "name": "uv", + "version": "0.13.4", + "source": {"registry": "https://pypi.org/simple"}, + } + ] +} + + +def test_exact_bootstrap_requirement() -> None: + assert bootstrap_requirement(PROJECT, LOCK) == "uv==0.13.4" + + +@pytest.mark.parametrize( + "requirement", [[], ["uv>=0.13.4"], ["uv==0.13.4", "uv==0.13.5"], ["pip==25.0.1"]] +) +def test_missing_ranged_or_duplicate_pin_fails(requirement: list[str]) -> None: + with pytest.raises(PolicyError): + bootstrap_requirement({"dependency-groups": {"bootstrap": requirement}}, LOCK) + + +@pytest.mark.parametrize("scenario", ["missing", "duplicate", "version", "source"]) +def test_lock_divergence_fails(scenario: str) -> None: + lock = copy.deepcopy(LOCK) + if scenario == "missing": + lock["package"] = [] + elif scenario == "duplicate": + lock["package"].append(lock["package"][0]) + elif scenario == "version": + lock["package"][0]["version"] = "0.11.29" + else: + lock["package"][0]["source"] = {"url": "https://example.test/uv.whl"} + with pytest.raises(PolicyError): + bootstrap_requirement(PROJECT, lock) + + +def test_main(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + project = tmp_path / "pyproject.toml" + lock = tmp_path / "uv.lock" + project.write_text( + '[dependency-groups]\nbootstrap=["uv==0.13.4"]\n', encoding="utf-8" + ) + lock.write_text( + '[[package]]\nname="uv"\nversion="0.13.4"\n' + 'source={registry="https://pypi.org/simple"}\n', + encoding="utf-8", + ) + args = ["--project", str(project), "--lock", str(lock)] + assert main(args) == 0 + assert capsys.readouterr().out == "uv==0.13.4\n" + assert main([*args, "--installed-version", "uv 0.13.4 (platform)"]) == 0 + assert main([*args, "--installed-version", "uv 0.11.29"]) == 1 + assert "installed uv version differs" in capsys.readouterr().err + project.write_text("broken = [", encoding="utf-8") + assert main(args) == 1 + project.unlink() + assert main(args) == 1 + + +def test_repository_has_one_pin_consumed_by_every_bootstrap() -> None: + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + lock = tomllib.loads((ROOT / "uv.lock").read_text(encoding="utf-8")) + requirement = bootstrap_requirement(project, lock) + assert ("uv", requirement.removeprefix("uv==")) in locked_packages(lock) + assert not any( + item.startswith("uv==") for item in project["dependency-groups"]["dev"] + ) + assert "bootstrap" not in project["tool"]["uv"].get("default-groups", ["dev"]) + steps: list[str] = [] + for path in (ROOT / ".github/workflows").glob("*.y*ml"): + for block in re.split(r"(?m)^ - ", path.read_text(encoding="utf-8")): + if "uses: astral-sh/setup-uv@" in block: + steps.append(block) + assert re.findall(r"(?m)^ version-file: (.+)$", block) == [ + "uv.lock" + ] + assert not re.search(r"(?m)^ version:", block) + assert len(steps) == 6 + hook = project["tool"]["semantic_release"]["build_command"] + assert 'UV_REQUIREMENT="$(python -m scripts.bootstrap_uv)"' in hook + assert 'python -m pip install --disable-pip-version-check "$UV_REQUIREMENT"' in hook + assert ( + 'python -m scripts.bootstrap_uv --installed-version "$(uv --version)"' in hook + ) + assert not re.search(r"uv==[0-9]", hook) + audit = (ROOT / "scripts/audit-dependencies.sh").read_text(encoding="utf-8") + assert audit.index("scripts.bootstrap_uv") < audit.index("uv audit") + assert '--installed-version "$(uv --version)"' in audit + + +def test_scanners_cannot_omit_bootstrap() -> None: + locked = locked_packages(LOCK) + uv_report: dict[str, Any] = { + "schema": {"version": "preview"}, + "summary": {"audited_packages": 0, "vulnerabilities": 0, "adverse_statuses": 0}, + "vulnerabilities": [], + "adverse_statuses": [], + } + with pytest.raises(PolicyError, match="every locked registry package"): + validate_uv_report(uv_report, (), locked, 0) + uv_report["summary"]["audited_packages"] = 1 + assert validate_uv_report(uv_report, (), locked, 0) + osv_report: dict[str, Any] = { + "results": [{"source": {"type": "lockfile", "path": "uv.lock"}, "packages": []}] + } + with pytest.raises(PolicyError): + validate_report(osv_report, (), locked, ROOT, 0) + + +def test_vulnerable_bootstrap_cannot_pass_either_scanner() -> None: + advisory = "GHSA-hq85-5cpj-mr62" + locked = {("uv", "0.11.29")} + finding: dict[str, Any] = { + "dependency": {"name": "uv", "version": "0.11.29"}, + "id": advisory, + "display_id": advisory, + "aliases": [], + "summary": None, + "description": None, + "link": None, + "fix_versions": ["0.13.4"], + "published": None, + "modified": None, + } + uv_report: dict[str, Any] = { + "schema": {"version": "preview"}, + "summary": {"audited_packages": 1, "vulnerabilities": 1, "adverse_statuses": 0}, + "vulnerabilities": [finding], + "adverse_statuses": [], + } + with pytest.raises(PolicyError, match="has a fixed release"): + validate_uv_report(uv_report, (), locked, 1) + osv_report: dict[str, Any] = { + "results": [ + { + "source": {"type": "lockfile", "path": "uv.lock"}, + "packages": [ + { + "package": { + "name": "uv", + "version": "0.11.29", + "ecosystem": "PyPI", + }, + "vulnerabilities": [{"id": advisory}], + "groups": [{"ids": [advisory]}], + } + ], + } + ] + } + with pytest.raises(PolicyError, match="unaccepted vulnerability"): + validate_report(osv_report, (), locked, ROOT, 1) diff --git a/tests/test_dependency_audit_workflow.py b/tests/test_dependency_audit_workflow.py index f9507ab..c8b4838 100644 --- a/tests/test_dependency_audit_workflow.py +++ b/tests/test_dependency_audit_workflow.py @@ -54,7 +54,7 @@ def _fixtures(accepted: bool) -> tuple[dict[str, Any], dict[str, Any], dict[str, uv_report: dict[str, Any] = { "schema": {"version": "preview"}, "summary": { - "audited_packages": 1, + "audited_packages": 2, "vulnerabilities": int(accepted), "adverse_statuses": 0, }, @@ -68,7 +68,19 @@ def _fixtures(accepted: bool) -> tuple[dict[str, Any], dict[str, Any], dict[str, package.update(vulnerabilities=[osv_finding], groups=[{"ids": [GHSA]}]) osv_report = { "results": [ - {"source": {"type": "lockfile", "path": "uv.lock"}, "packages": [package]} + { + "source": {"type": "lockfile", "path": "uv.lock"}, + "packages": [ + package, + { + "package": { + "name": "uv", + "version": "0.13.4", + "ecosystem": "PyPI", + } + }, + ], + } ] } return ( @@ -88,6 +100,7 @@ def _fixtures(accepted: bool) -> tuple[dict[str, Any], dict[str, Any], dict[str, "uv-fixed", "osv-unrelated", "build-failure", + "bootstrap-mismatch", ], ) def test_end_to_end_scanner_policy(tmp_path: Path, scenario: str) -> None: @@ -101,8 +114,12 @@ def test_end_to_end_scanner_policy(tmp_path: Path, scenario: str) -> None: shutil.copyfile( ROOT / ".github/osv-scanner-empty.toml", work / ".github/osv-scanner-empty.toml" ) + (work / "pyproject.toml").write_text( + '[dependency-groups]\nbootstrap=["uv==0.13.4"]\n', encoding="utf-8" + ) (work / "uv.lock").write_text( - '[[package]]\nname="example"\nversion="1.0"\nsource={registry="https://pypi.org/simple"}\n', + '[[package]]\nname="example"\nversion="1.0"\nsource={registry="https://pypi.org/simple"}\n' + '[[package]]\nname="uv"\nversion="0.13.4"\nsource={registry="https://pypi.org/simple"}\n', encoding="utf-8", ) accepted = scenario != "clean" @@ -129,11 +146,14 @@ def test_end_to_end_scanner_policy(tmp_path: Path, scenario: str) -> None: f"#!{sys.executable}\n" + """import os, sys from pathlib import Path +if sys.argv[1] == "--version": + print(os.environ.get("UV_VERSION", "uv 0.13.4")) + raise SystemExit(0) if sys.argv[1] == "audit": assert "--no-config" in sys.argv and "--locked" in sys.argv print(Path(os.environ["UV_FIXTURE"]).read_text()) raise SystemExit(int(os.environ["UV_EXIT"])) -os.execv(sys.executable, [sys.executable, *sys.argv[3:]]) +os.execv(sys.executable, [sys.executable, *sys.argv[sys.argv.index("python") + 1:]]) """, encoding="utf-8", ) @@ -161,6 +181,7 @@ def test_end_to_end_scanner_policy(tmp_path: Path, scenario: str) -> None: "PATH": f"{binaries}{os.pathsep}{os.environ['PATH']}", "RUNNER_TEMP": str(tmp_path), "GITHUB_STEP_SUMMARY": str(summary), + "UV_VERSION": "uv 0.11.29" if scenario == "bootstrap-mismatch" else "uv 0.13.4", "UV_FIXTURE": str(tmp_path / "uv.json"), "OSV_FIXTURE": str(tmp_path / "osv.json"), "UV_EXIT": "2" if scenario == "uv-operational" else str(int(accepted)), @@ -179,6 +200,11 @@ def test_end_to_end_scanner_policy(tmp_path: Path, scenario: str) -> None: result.stdout + result.stderr ) output = tmp_path / "optimizer-dependency-audit" + if scenario == "bootstrap-mismatch": + assert "installed uv version differs" in result.stderr + assert not (output / "uv.json").exists() + assert not (output / "osv.json").exists() + return assert json.loads((output / "uv.json").read_text(encoding="utf-8")) == uv_report if scenario != "build-failure": assert ( diff --git a/uv.lock b/uv.lock index 935bee7..789dd9d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.14" [[package]] @@ -8,6 +8,9 @@ version = "0.2.6" source = { editable = "." } [package.dev-dependencies] +bootstrap = [ + { name = "uv" }, +] dev = [ { name = "prek" }, { name = "pydoclint" }, @@ -23,6 +26,7 @@ dev = [ [package.metadata] [package.metadata.requires-dev] +bootstrap = [{ name = "uv", specifier = "==0.13.4" }] dev = [ { name = "prek", specifier = ">=0.4.10" }, { name = "pydoclint", specifier = ">=0.9.1" }, @@ -301,3 +305,29 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "uv" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/55/609455d21e87430c11fe734abfced50a3bdf177b2f49d41fbb19c199cbc3/uv-0.13.4.tar.gz", hash = "sha256:b0bff5b9a5061f7a35b1d0c1a94c19af134222c48eb7aadc42f85a038be1b248", size = 7482486, upload-time = "2026-10-20T15:12:06.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/bd/4349da5356b17a1c0e36c8552c809925f86f6292de9b1f599f01e8e8093d/uv-0.13.4-py3-none-linux_armv6l.whl", hash = "sha256:205a4f93e568fb24f45a2f3834f1c0a8f1adb5f396bf473c71030ed707dce048", size = 18272390, upload-time = "2026-10-20T15:11:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/0cb77a1ae43959772f8f7873eb1f4061907fbb1694ce55c79b231f639b1d/uv-0.13.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b4bbacea78c9db136cd2ec3147cbfd9ed910b4db5e1877109bd74d0e73bb2eaf", size = 17384113, upload-time = "2026-10-20T15:11:07.172Z" }, + { url = "https://files.pythonhosted.org/packages/26/44/5a8733dd1b2e8d6f9cffd1ff6428a3e11dcc7e146a7657e32cfb8d9fb4a4/uv-0.13.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:95f4eb2b991b53b50ec2b18ab612fca8ba1e7a75a47af0331fb8bcd21da3eb03", size = 14433723, upload-time = "2026-10-20T15:11:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/f8/39/be14f051bf34a1822d634819890a2d0cba3b69101e633fb7b9f54d7b4020/uv-0.13.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e90ab42825587821096cb0d071e387468da38cfa22818c91f9c0b3c0453048e8", size = 17816443, upload-time = "2026-10-20T15:11:13.674Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/5df0f8bc6367dd37b98432a7c462e2918554f6db2a71211574852b8f0727/uv-0.13.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3a9dcb6762c3ba249d8c084315ba03839082af3ece3536cc4164ec735c351d68", size = 17828025, upload-time = "2026-10-20T15:11:16.806Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9c/2fed5357700b7c304b94214979e4efe680c3bc6e337a97eedaa372fe0947/uv-0.13.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ae6ff2b07892379dc767c9064c2b8bbfac88e767edac7ce28e1b0f15d86abb4", size = 17843737, upload-time = "2026-10-20T15:11:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/4c/2e/ee026284f096236dc3e1abed0316f9edab65c0ea176ea0072c064e62478d/uv-0.13.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25cc14ff4fd8335d3be2cc493a621a003fdafcb0fb1c953c905daabe4def5ab8", size = 18576350, upload-time = "2026-10-20T15:11:24.035Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f1/67fbdb649c8c5d0cb38ea74829080479b77c8631cda5d77fc1b28f027ca7/uv-0.13.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77062b02856c74697f48328d8a6583ab07e6da0a993e86c2db46f3d1da27c637", size = 19831234, upload-time = "2026-10-20T15:11:27.627Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/7d49e3a1a3e98e5244fa38e0c964dc8de568bf3bb101257a63dc2602efed/uv-0.13.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb0e2028d9a8929884344afb7d91d0f65d9783f640d28aede89fca3602bedde9", size = 19702886, upload-time = "2026-10-20T15:11:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a1/de02c0913726728eca56ee258875cbba9c7b0572c63f494fd9f55c7852e3/uv-0.13.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e4332791b4d55b6dde8695b7fa6079b26991519f44760887c5dfbd5ca4fe53", size = 17181168, upload-time = "2026-10-20T15:11:34.583Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/f4a3c7377674366798c56aabd0f9a1f05687c3939f5ba857a3ea6093ff84/uv-0.13.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7b3cdf868e5415631fe797b648c0d6945d38d388f23367bdd0b6332e80a1fcb0", size = 16502020, upload-time = "2026-10-20T15:11:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/ce79657c5a0f51b602db9a663529baafff03b9fe8cc41e09ad3f7246b88a/uv-0.13.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4e6b08578c920b86910897f36bb6da7229c8e7e0187976570e2a538cb28b777e", size = 18000980, upload-time = "2026-10-20T15:11:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/7d77022d5661f8e01578bc5423bf02af408220e7e7ac5ee15eb94742c7cb/uv-0.13.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d86a5dec8ab30c70519c71ca5085c8aaf28664836cbc600951cedaa3fc678cbf", size = 18179083, upload-time = "2026-10-20T15:11:45.191Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/1bf0324800d38009774ba087a6fbffddc76a3e5bf42c081b9298b1236fb5/uv-0.13.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5d89d1028c2e22977d9215cbb79cfe12559cb0c8a507a12e2f3a7e47930cf1bf", size = 18010389, upload-time = "2026-10-20T15:11:48.963Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ec/f352ca2cc3b4b6191bb4ec8cdf4bc190eed0ba8bda4c33b7fd66c5d33cec/uv-0.13.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:52b5c753a7c2d5f8323a70bb4c93118988356331d0758afa6446c81f974e7196", size = 19004998, upload-time = "2026-10-20T15:11:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/fc05ff4292edfe785b47874ec86d3d83ca2bf735cff8a9d2170ffb66ad83/uv-0.13.4-py3-none-win32.whl", hash = "sha256:d333f01852b817ba05612de31474739da1af7e5463d119b30a9c4331eba1f408", size = 16190502, upload-time = "2026-10-20T15:11:56.657Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b9/b526eab6c72eacc705a58378f6850fde046b28c86fed01bd42da61367c6b/uv-0.13.4-py3-none-win_amd64.whl", hash = "sha256:b2ff80397273fff0e02c20eacae6882834c811827d6fc08f658db156fe754386", size = 15331833, upload-time = "2026-10-20T15:11:59.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/4d5fa89d3d10ff6a84e547ef6e02c45b2f2d50e66682da778acf60db63f1/uv-0.13.4-py3-none-win_arm64.whl", hash = "sha256:deffa5a4afdb48f3e44fb26e80377d256b330ddcad0373daff4b54f6162b43a4", size = 15690151, upload-time = "2026-10-20T15:12:02.74Z" }, +]