diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml new file mode 100644 index 00000000..dda38497 --- /dev/null +++ b/.github/actions/setup-uv/action.yml @@ -0,0 +1,9 @@ +name: Set up uv +description: Install the lowest uv version allowed by the root pyproject.toml. +runs: + using: composite + steps: + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + version-file: pyproject.toml + resolution-strategy: lowest diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2b5719c7..3f005d65 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -59,7 +59,7 @@ updates: patterns: - "*" # Compose manifests live at the root; Dockerfiles live under src/. - # Update uv in the Dockerfiles together with every workflow's UV_VERSION manually. + # Keep the Dockerfile uv image tags at the lower bound in pyproject.toml. - package-ecosystem: "docker-compose" directory: "/" schedule: diff --git a/.github/verify_deps_sync.py b/.github/verify_deps_sync.py index 8f622000..73d1b68a 100644 --- a/.github/verify_deps_sync.py +++ b/.github/verify_deps_sync.py @@ -23,6 +23,7 @@ PRECOMMIT_CONFIG = ".pre-commit-config.yaml" PYPROJECTS = ["./pyproject.toml", "./client/pyproject.toml"] TRACKED_DEPS = ("uv", "ruff", "ty", "prek") +UV_REQUIRED_VERSION_PROJECTS = {"./pyproject.toml"} logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -52,10 +53,31 @@ def add_dep(deps: dict[str, list[dict[str, str]]], dep: str, file: str, group: s deps[pkg].append({"file": file, "version": parsed["version"], "group": group}) -def add_pyproject_deps(deps: dict[str, list[dict[str, str]]], pyproject_path: str) -> None: +def uv_lower_bound(required_version: str, source: str, troubles: list[str]) -> str | None: + versions = re.findall(r"(?:^|,)\s*>=\s*(\d+(?:\.\d+){1,2})(?=\s*(?:,|$))", required_version) + if len(versions) != 1: + troubles.append( + f"{source}: [tool.uv].required-version must have one >= lower bound, found {required_version!r}" + ) + return None + return versions[0] + + +def add_pyproject_deps(deps: dict[str, list[dict[str, str]]], pyproject_path: str, troubles: list[str]) -> None: with Path(pyproject_path).open("rb") as f: pyproject: dict[str, Any] = tomllib.load(f) + tool_config = pyproject.get("tool", {}) + uv_config = tool_config.get("uv") if isinstance(tool_config, dict) else None + if pyproject_path in UV_REQUIRED_VERSION_PROJECTS or uv_config is not None: + required_version = uv_config.get("required-version") if isinstance(uv_config, dict) else None + if isinstance(required_version, str): + lower_bound = uv_lower_bound(required_version, pyproject_path, troubles) + if lower_bound is not None: + deps["uv"].append({"file": pyproject_path, "version": f"=={lower_bound}", "group": "required-version"}) + else: + troubles.append(f"{pyproject_path}: [tool.uv].required-version is missing") + project = pyproject.get("project", {}) if isinstance(project, dict): dependencies = project.get("dependencies", []) @@ -80,6 +102,8 @@ def add_pyproject_deps(deps: dict[str, list[dict[str, str]]], pyproject_path: st def main() -> None: deps: dict[str, list[dict[str, str]]] = {dep: [] for dep in TRACKED_DEPS} + troubles = [] + setup_action_calls = 0 for dockerfile in DOCKERFILES: dockerfile_content = Path(dockerfile).read_text(encoding="utf-8") @@ -105,20 +129,41 @@ def main() -> None: }) for pyproject_path in PYPROJECTS: - add_pyproject_deps(deps, pyproject_path) + add_pyproject_deps(deps, pyproject_path, troubles) for workflow_file in Path(".github/workflows").glob("*.yml"): - with workflow_file.open("r", encoding="utf-8") as f: - workflow = yaml.safe_load(f) or {} - env = workflow.get("env", {}) - if "UV_VERSION" in env: - deps["uv"].append({ - "file": str(workflow_file), - "version": f"=={env['UV_VERSION'].lstrip('v')}", - "group": "workflow", - }) + workflow_content = workflow_file.read_text(encoding="utf-8") + if "astral-sh/setup-uv@" in workflow_content: + troubles.append(f"{workflow_file}: use the shared .github/actions/setup-uv action") + setup_action_calls += workflow_content.count("./.github/actions/setup-uv") + + action_path = Path(".github/actions/setup-uv/action.yml") + action = yaml.safe_load(action_path.read_text(encoding="utf-8")) + setup_steps = [ + step + for step in action.get("runs", {}).get("steps", []) + if str(step.get("uses", "")).startswith("astral-sh/setup-uv@") + ] + if setup_action_calls == 0: + troubles.append(".github/workflows: no shared .github/actions/setup-uv calls found") + if len(setup_steps) != 1: + troubles.append(f"{action_path}: expected one astral-sh/setup-uv step") + else: + setup = setup_steps[0].get("with", {}) + if ( + "version" in setup + or setup.get("version-file") != "pyproject.toml" + or setup.get("resolution-strategy") != "lowest" + ): + troubles.append(f"{action_path}: install uv from pyproject.toml with resolution-strategy: lowest") + else: + root = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + required = root.get("tool", {}).get("uv", {}).get("required-version") + if isinstance(required, str): + lower_bound = uv_lower_bound(required, "pyproject.toml", troubles) + if lower_bound is not None: + deps["uv"].append({"file": str(action_path), "version": f"=={lower_bound}", "group": "setup-uv"}) - troubles = [] for dep, versions in deps.items(): if not versions: troubles.append(f"{dep}: no tracked version found") diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 5332ae66..321488ad 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -8,7 +8,6 @@ on: env: PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: docker: @@ -19,9 +18,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Build, run & check docker env: SUPERADMIN_LOGIN: dummy_login @@ -51,9 +48,7 @@ jobs: with: python-version: ${{ matrix.python }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Install project run: | uv pip install --system -e client/. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d63d0865..17eb0bf7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,9 +3,6 @@ on: push: branches: main -env: - UV_VERSION: "0.11.14" - jobs: gh-pages: runs-on: ubuntu-latest @@ -17,9 +14,7 @@ jobs: with: python-version: "3.10" architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Install dependencies run: uv pip install --system -e "client/.[docs]" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e4f2f90e..111d33f3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,7 +6,6 @@ on: env: PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: pypi: @@ -18,9 +17,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Install dependencies run: uv pip install --system setuptools wheel twine --upgrade - name: Build and publish @@ -47,9 +44,7 @@ jobs: with: python-version: ${{ matrix.python }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Install package run: | uv pip install --system pyroclient diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index c72f190e..b88f276f 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -4,9 +4,6 @@ on: pull_request: branches: main -env: - UV_VERSION: "0.11.14" - jobs: docs-client: runs-on: ubuntu-latest @@ -18,9 +15,7 @@ jobs: with: python-version: "3.10" architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Install dependencies run: uv pip install --system -e "client/.[docs]" diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 1c49f10e..267cec89 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,7 +9,6 @@ env: BACKEND_IMAGE_NAME: alert-api DOCKERHUB_USER: ${{ secrets.DOCKERHUB_LOGIN }} PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: docker: @@ -20,9 +19,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Resolve image tag run: | if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml index 8cabddec..302d2202 100644 --- a/.github/workflows/scripts.yml +++ b/.github/workflows/scripts.yml @@ -8,7 +8,6 @@ on: env: PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: end-to-end: @@ -19,9 +18,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Run the backend & the test env: SUPERUSER_LOGIN: dummy_login diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 950948a9..dc06fc92 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -8,7 +8,6 @@ on: env: PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: ruff: @@ -19,9 +18,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Run ruff run: | uv run --group quality ruff --version @@ -35,9 +32,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Run ty run: | uv run --group quality ty --version @@ -51,9 +46,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Run prek hooks run: | uv run --group quality prek --version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 01a3dd19..e7606de3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,6 @@ on: env: PYTHON_VERSION: "3.11" - UV_VERSION: "0.11.14" jobs: pytest: @@ -19,9 +18,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv # Build while PostgreSQL and S3 download and become healthy. - parallel: - name: Start test services @@ -81,9 +78,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: astral-sh/setup-uv@v7 - with: - version: ${{ env.UV_VERSION }} + - uses: ./.github/actions/setup-uv - name: Run client tests env: SUPERADMIN_LOGIN: dummy_login diff --git a/pyproject.toml b/pyproject.toml index fa0a2854..027fc999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [] [tool.uv] package = false +required-version = ">=0.13.4,<1.0.0" [tool.uv.sources] pyroclient = { path = "client", editable = true } diff --git a/src/Dockerfile b/src/Dockerfile index b54d8cf1..787343f5 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get -y update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /bin/uv COPY pyproject.toml uv.lock /app/ COPY client/pyproject.toml /app/client/pyproject.toml diff --git a/src/Dockerfile.test b/src/Dockerfile.test index 676a4d7c..5a46e167 100644 --- a/src/Dockerfile.test +++ b/src/Dockerfile.test @@ -14,7 +14,7 @@ RUN apt-get -y update \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p client -RUN --mount=from=ghcr.io/astral-sh/uv:0.11.14,source=/uv,target=/bin/uv \ +RUN --mount=from=ghcr.io/astral-sh/uv:0.13.4,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ diff --git a/uv.lock b/uv.lock index 4f8db6a4..3cf0a0ec 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11, <4" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -296,11 +296,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.10.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" }, ] [[package]]