From 8632e39427ff2dca735dc43310a952f0418306eb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 2 Sep 2026 16:40:43 -0500 Subject: [PATCH 1/2] Export UV_PYTHON as a version, not an interpreter path An absolute interpreter path outranks a virtual environment the job activates later, so `uv pip install` targeted the setup-python interpreter instead of the venv. mongo-python-driver's minimum-dependency job created a venv, activated it, installed into the interpreter behind it, and passed. A version request still overrides pyproject.toml and .python-version, and still beats a later setup-python on PATH, but a matching activated venv now wins. UV_PYTHON_PREFERENCE keeps the promise that uv never downloads an interpreter. --- python/README.md | 11 +++++++++-- python/setup/action.yml | 12 +++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/python/README.md b/python/README.md index 1f04b4d..f9a4a18 100644 --- a/python/README.md +++ b/python/README.md @@ -15,8 +15,15 @@ repos repeat in every CI job. ``` Python comes from `actions/setup-python`, which is faster than having uv download -a managed interpreter. uv is then pinned to that interpreter through `UV_PYTHON`, -so the project's `pyproject.toml` and `.python-version` do not change the choice. +a managed interpreter. The action then exports `UV_PYTHON` as the requested +version, so the project's `pyproject.toml` and `.python-version` do not change +the choice, and `UV_PYTHON_PREFERENCE=only-system`, so uv never downloads an +interpreter of its own. + +`UV_PYTHON` holds a version rather than an interpreter path. A path would also +outrank a virtual environment that the job activates later, so a job doing its +own `uv venv` would find `uv pip install` targeting the interpreter behind its +back. The action runs no just recipes. A job that needs project dependencies runs its own `just install` step after this one. diff --git a/python/setup/action.yml b/python/setup/action.yml index ce2e554..93699b7 100644 --- a/python/setup/action.yml +++ b/python/setup/action.yml @@ -16,20 +16,22 @@ runs: steps: # Runner images already ship these versions, so uv downloads no interpreter. - name: Install Python - id: setup-python uses: actions/setup-python@v7 with: python-version: ${{ inputs.python-version }} allow-prereleases: ${{ inputs.allow-prereleases }} - - name: Point uv at that interpreter + - name: Point uv at that Python shell: bash env: - PYTHON_PATH: ${{ steps.setup-python.outputs.python-path }} + PYTHON_VERSION: ${{ inputs.python-version }} # GITHUB_ENV is the only way a composite action exports to later steps. run: | # zizmor: ignore[github-env] - # An absolute path pins uv here, ignoring .python-version. - echo "UV_PYTHON=$PYTHON_PATH" >> "$GITHUB_ENV" + # A version, not an interpreter path: a path also outranks a virtual + # environment the job activates later, which silently retargets `uv pip`. + echo "UV_PYTHON=$PYTHON_VERSION" >> "$GITHUB_ENV" + # setup-python already provided the interpreter, so never download one. + echo "UV_PYTHON_PREFERENCE=only-system" >> "$GITHUB_ENV" - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 From 96ae6aac688cf66669536c0fb4460b72ae86aca1 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Sep 2026 13:31:11 -0500 Subject: [PATCH 2/2] Reject a python-version with embedded whitespace Writing the raw input straight to GITHUB_ENV meant a newline in python-version split into a second line there, and the runner parses GITHUB_ENV one KEY=VALUE pair per line, so that line becomes an arbitrary extra environment variable for every later step in the job. Trailing or leading whitespace is trimmed first, so a caller who writes the input as a `|` block scalar still works. What remains must be a single token. --- python/setup/action.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/python/setup/action.yml b/python/setup/action.yml index 93699b7..982b86c 100644 --- a/python/setup/action.yml +++ b/python/setup/action.yml @@ -27,9 +27,19 @@ runs: PYTHON_VERSION: ${{ inputs.python-version }} # GITHUB_ENV is the only way a composite action exports to later steps. run: | # zizmor: ignore[github-env] + shopt -s extglob + # Trim incidental whitespace, e.g. from a caller using a `|` block + # scalar. What remains must be a single token, or a newline here + # would inject an arbitrary extra line into GITHUB_ENV. + VERSION="${PYTHON_VERSION##+([[:space:]])}" + VERSION="${VERSION%%+([[:space:]])}" + if [[ "$VERSION" == *[[:space:]]* ]]; then + echo "::error::python-version must be a single version, got: $PYTHON_VERSION" + exit 1 + fi # A version, not an interpreter path: a path also outranks a virtual # environment the job activates later, which silently retargets `uv pip`. - echo "UV_PYTHON=$PYTHON_VERSION" >> "$GITHUB_ENV" + echo "UV_PYTHON=$VERSION" >> "$GITHUB_ENV" # setup-python already provided the interpreter, so never download one. echo "UV_PYTHON_PREFERENCE=only-system" >> "$GITHUB_ENV"