From 652721249377ff74d56a53ca29fca3999a352c36 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:01:51 -0700 Subject: [PATCH 1/3] fix(ci): pin uv tool version --- pyproject.toml | 6 ++++++ tests/scripts/uv_tool_version.test.mjs | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/scripts/uv_tool_version.test.mjs diff --git a/pyproject.toml b/pyproject.toml index 9001760d..1613dc1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,12 @@ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.uv] +# setup-uv reads this before falling back to its mutable remote version manifest. +# An exact version keeps every CI job on the reviewed tool release and removes that +# metadata fetch from the setup path. +required-version = "==0.9.17" + [project] name = "visionset" dynamic = ["version"] diff --git a/tests/scripts/uv_tool_version.test.mjs b/tests/scripts/uv_tool_version.test.mjs new file mode 100644 index 00000000..cf327f8d --- /dev/null +++ b/tests/scripts/uv_tool_version.test.mjs @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +const ROOT = join(fileURLToPath(new URL("../..", import.meta.url))); + +const read = (...parts) => readFileSync(join(ROOT, ...parts), "utf8"); + +test("the repository declares an exact uv version for setup-uv", () => { + const pyproject = read("pyproject.toml"); + const toolUvStart = pyproject.indexOf("[tool.uv]"); + assert.notEqual(toolUvStart, -1, "pyproject.toml must declare a [tool.uv] section"); + const nextSection = pyproject.indexOf("\n[", toolUvStart + 1); + const toolUv = pyproject.slice(toolUvStart, nextSection === -1 ? undefined : nextSection); + const requiredVersion = /^required-version\s*=\s*"==([0-9]+\.[0-9]+\.[0-9]+)"$/m.exec(toolUv); + assert.ok( + requiredVersion, + "pyproject.toml must declare an exact [tool.uv] required-version so setup-uv does not resolve latest", + ); +}); From ec870462d506f78988951fbfca74a5a95daffbd5 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:06:14 -0700 Subject: [PATCH 2/3] fix(ci): align uv with container runtime --- pyproject.toml | 2 +- tests/scripts/uv_tool_version.test.mjs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1613dc1f..34f4481e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" # setup-uv reads this before falling back to its mutable remote version manifest. # An exact version keeps every CI job on the reviewed tool release and removes that # metadata fetch from the setup path. -required-version = "==0.9.17" +required-version = "==0.12.3" [project] name = "visionset" diff --git a/tests/scripts/uv_tool_version.test.mjs b/tests/scripts/uv_tool_version.test.mjs index cf327f8d..c75a57f5 100644 --- a/tests/scripts/uv_tool_version.test.mjs +++ b/tests/scripts/uv_tool_version.test.mjs @@ -19,4 +19,9 @@ test("the repository declares an exact uv version for setup-uv", () => { requiredVersion, "pyproject.toml must declare an exact [tool.uv] required-version so setup-uv does not resolve latest", ); + assert.equal( + requiredVersion[1], + "0.12.3", + "the required version must match the uv version in the repository's container images", + ); }); From 5e982280671b25fee1bfb25fe1c7d933d3808afc Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:18:14 -0700 Subject: [PATCH 3/3] fix(ci): pin setup-uv version --- .github/workflows/ci.yml | 15 +++++++++--- pyproject.toml | 6 ----- tests/scripts/uv_tool_version.test.mjs | 33 +++++++++++++------------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 761d28aa..7d82fc9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true # `--locked`, not a bare `uv sync`, and it does two jobs. It refuses to @@ -217,6 +218,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Sync environment @@ -548,6 +550,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Sync environment @@ -641,6 +644,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Install @@ -715,6 +719,7 @@ jobs: if: needs.wheel.result == 'success' && (needs.changes.result != 'success' || needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true') uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Download the distribution the `wheel` job already built @@ -776,6 +781,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Install @@ -882,6 +888,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Sync environment with the local-inference extra @@ -948,6 +955,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Sync environment with the format groups @@ -1017,6 +1025,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs['browser-models'] == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true # The sparse upstream checkout and the checkpoint download, never the exported @@ -1031,9 +1040,8 @@ jobs: # `--group browser-models` is repeated on every `uv run` below rather than left to # this sync. `uv run` performs its own sync first, and whether that prunes a group the - # command did not name is behaviour this workflow should not have to know: `setup-uv` - # is not version-pinned, so the uv deciding it is whichever one is current on the day. - # Naming the group each time costs nothing and makes the step say what it needs. + # command did not name is behaviour this workflow should not have to know. Naming the + # group each time costs nothing and makes the step say what it needs. - name: Sync environment with the browser-models group if: needs.changes.result != 'success' || needs.changes.outputs['browser-models'] == 'true' run: uv sync --locked --group browser-models @@ -1238,6 +1246,7 @@ jobs: if: needs.changes.result != 'success' || needs.changes.outputs.python == 'true' uses: astral-sh/setup-uv@v7 with: + version: "0.12.3" enable-cache: true - name: Sync environment diff --git a/pyproject.toml b/pyproject.toml index 34f4481e..9001760d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,12 +2,6 @@ requires = ["hatchling"] build-backend = "hatchling.build" -[tool.uv] -# setup-uv reads this before falling back to its mutable remote version manifest. -# An exact version keeps every CI job on the reviewed tool release and removes that -# metadata fetch from the setup path. -required-version = "==0.12.3" - [project] name = "visionset" dynamic = ["version"] diff --git a/tests/scripts/uv_tool_version.test.mjs b/tests/scripts/uv_tool_version.test.mjs index c75a57f5..fe25ac41 100644 --- a/tests/scripts/uv_tool_version.test.mjs +++ b/tests/scripts/uv_tool_version.test.mjs @@ -8,20 +8,21 @@ const ROOT = join(fileURLToPath(new URL("../..", import.meta.url))); const read = (...parts) => readFileSync(join(ROOT, ...parts), "utf8"); -test("the repository declares an exact uv version for setup-uv", () => { - const pyproject = read("pyproject.toml"); - const toolUvStart = pyproject.indexOf("[tool.uv]"); - assert.notEqual(toolUvStart, -1, "pyproject.toml must declare a [tool.uv] section"); - const nextSection = pyproject.indexOf("\n[", toolUvStart + 1); - const toolUv = pyproject.slice(toolUvStart, nextSection === -1 ? undefined : nextSection); - const requiredVersion = /^required-version\s*=\s*"==([0-9]+\.[0-9]+\.[0-9]+)"$/m.exec(toolUv); - assert.ok( - requiredVersion, - "pyproject.toml must declare an exact [tool.uv] required-version so setup-uv does not resolve latest", - ); - assert.equal( - requiredVersion[1], - "0.12.3", - "the required version must match the uv version in the repository's container images", - ); +test("CI requests an exact reviewed uv version", () => { + const workflow = read(".github", "workflows", "ci.yml"); + const setupUvCount = [...workflow.matchAll(/uses: astral-sh\/setup-uv@v7/g)].length; + const installSteps = workflow + .split(/^ {6}- name: Install uv\n/m) + .slice(1) + .map((step) => step.split(/^ {6}- /m, 1)[0]); + + assert.ok(setupUvCount > 0, "CI must install uv"); + assert.equal(installSteps.length, setupUvCount, "every setup-uv use must have an Install uv step"); + for (const step of installSteps) { + assert.match( + step, + /^ {10}version: "0\.12\.3"$/m, + "each setup-uv action must request the reviewed version instead of resolving latest", + ); + } });