From 7c9d0fa7c994f5f52585f47ac40ba9bb8dc5970e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 6 May 2026 11:37:09 +0200 Subject: [PATCH 01/17] chore(downstream): uv driver and TOML recipes for downstream checks Replace per-project shell scripts with a PEP 723 uv-runnable driver and validated recipe files under downstream/recipes/. Environment kinds (uv-venv, stdlib-venv, none) pair with nested [environment.install] in each TOML. Document the flow in downstream/README.md and RELEASING.rst. Co-authored-by: Cursor AI Co-authored-by: Composer --- RELEASING.rst | 3 +- downstream/.gitignore | 1 + downstream/README.md | 39 ++- downstream/conda.sh | 12 - downstream/datasette.sh | 10 - downstream/devpi.sh | 13 - downstream/hatch.sh | 10 - downstream/pytest.sh | 10 - downstream/python-lsp-server.sh | 31 -- downstream/recipes/conda.toml | 19 ++ downstream/recipes/datasette.toml | 15 + downstream/recipes/devpi.toml | 24 ++ downstream/recipes/hatch.toml | 22 ++ downstream/recipes/pytest.toml | 13 + downstream/recipes/python-lsp-server.toml | 12 + downstream/recipes/tox.toml | 15 + downstream/run_downstream.py | 372 ++++++++++++++++++++++ downstream/tox.sh | 9 - 18 files changed, 532 insertions(+), 98 deletions(-) delete mode 100755 downstream/conda.sh delete mode 100755 downstream/datasette.sh delete mode 100755 downstream/devpi.sh delete mode 100755 downstream/hatch.sh delete mode 100755 downstream/pytest.sh delete mode 100644 downstream/python-lsp-server.sh create mode 100644 downstream/recipes/conda.toml create mode 100644 downstream/recipes/datasette.toml create mode 100644 downstream/recipes/devpi.toml create mode 100644 downstream/recipes/hatch.toml create mode 100644 downstream/recipes/pytest.toml create mode 100644 downstream/recipes/python-lsp-server.toml create mode 100644 downstream/recipes/tox.toml create mode 100644 downstream/run_downstream.py delete mode 100755 downstream/tox.sh diff --git a/RELEASING.rst b/RELEASING.rst index 3d6ba16c..d87ace73 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -3,7 +3,8 @@ Release Procedure #. Dependening on the magnitude of the changes in the release, consider testing some of the large downstream users of pluggy against the upcoming release. - You can do so using the scripts in the ``downstream/`` directory. + You can do so with ``uv run downstream/run_downstream.py`` (option + ``--list`` lists recipes). #. From a clean work tree, execute:: diff --git a/downstream/.gitignore b/downstream/.gitignore index 0dc1814e..bdd5becb 100644 --- a/downstream/.gitignore +++ b/downstream/.gitignore @@ -3,4 +3,5 @@ /devpi/ /hatch/ /pytest/ +/python-lsp-server/ /tox/ diff --git a/downstream/README.md b/downstream/README.md index ff420e7d..68c3721a 100644 --- a/downstream/README.md +++ b/downstream/README.md @@ -1,2 +1,37 @@ -This directory contains scripts for testing some downstream projects -against your current pluggy worktree. +This directory contains tooling for testing some downstream projects against +your current pluggy checkout. + +Each project is described by a TOML recipe in `recipes/` with three top-level +sections plus tests: + +1. **`[git]`** — repository URL, local directory name (`into`), optional `shallow` + (default: shallow clone). +2. **`[environment]`** — how the Python environment is created **once** (skipped + if `venv/pyvenv.cfg` already exists): + - `kind = "uv-venv"` — `uv venv venv` in the clone root. + - `kind = "stdlib-venv"` — `python -m venv venv` in the clone root. + - `kind = "none"` — do not create a venv (e.g. conda’s `dev/start` workflow). + **`[environment.install]`** holds **`editables`**: strings passed as consecutive + **`-e`** arguments to `uv pip install` or `pip install` (same forms you would + type on the command line, e.g. `".[dev]"`, `"../.."`). Optional + **`[environment.install.uv]`** (`groups`, `packages`) or + **`[environment.install.pip]`** (`packages`) match the environment kind. For + `kind = "none"`, **`[environment.install.bootstrap]`** with `source` is + required (shell script sourced before pip). Rerun tests without reinstall: + `uv run downstream/run_downstream.py --skip-install RECIPE`. +3. **`[[test]]`** — one or more `argv` arrays. For `uv-venv` / `stdlib-venv`, the + driver sets **`VIRTUAL_ENV`** and prepends the venv’s `bin` (or `Scripts` on + Windows) to **`PATH`**, so test commands can use bare names like `pytest`. + Install only: `--only-install`. + +Run the driver (PEP 723 in `run_downstream.py`): + +```bash +uv run downstream/run_downstream.py --list +uv run downstream/run_downstream.py pytest +uv run downstream/run_downstream.py pytest --skip-install +``` + +Requirements: Python 3.11+ for the driver, `git`, and `uv` where recipes use it. +Recipes using `uv-venv` / `stdlib-venv` get an activated-style environment for +install and test subprocesses; conda recipes use `bash` and their own `dev/start`. diff --git a/downstream/conda.sh b/downstream/conda.sh deleted file mode 100755 index 685d08d4..00000000 --- a/downstream/conda.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d conda ]]; then - git clone https://github.com/conda/conda -fi -pushd conda && trap popd EXIT -git pull -set +eu -source dev/start -set -eu -pip install -e ../../ -pytest -m "not integration and not installed" diff --git a/downstream/datasette.sh b/downstream/datasette.sh deleted file mode 100755 index 7d3c5586..00000000 --- a/downstream/datasette.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d datasette ]]; then - git clone https://github.com/simonw/datasette -fi -pushd datasette && trap popd EXIT -git pull -python -m venv venv -venv/bin/pip install -e .[test] -e ../.. -venv/bin/pytest diff --git a/downstream/devpi.sh b/downstream/devpi.sh deleted file mode 100755 index 7ef09c8d..00000000 --- a/downstream/devpi.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d devpi ]]; then - git clone https://github.com/devpi/devpi -fi -pushd devpi && trap popd EXIT -git pull -python -m venv venv -venv/bin/pip install -r dev-requirements.txt -e ../.. -venv/bin/pytest common -venv/bin/pytest server -venv/bin/pytest client -venv/bin/pytest web diff --git a/downstream/hatch.sh b/downstream/hatch.sh deleted file mode 100755 index 933e0f63..00000000 --- a/downstream/hatch.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d hatch ]]; then - git clone https://github.com/pypa/hatch -fi -pushd hatch && trap popd EXIT -git pull -python -m venv venv -venv/bin/pip install -e . -e ./backend -e ../.. -venv/bin/hatch run dev diff --git a/downstream/pytest.sh b/downstream/pytest.sh deleted file mode 100755 index 5afc5612..00000000 --- a/downstream/pytest.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d pytest ]]; then - git clone https://github.com/pytest-dev/pytest -fi -pushd pytest && trap popd EXIT -git pull -python -m venv venv -venv/bin/pip install -e .[testing] -e ../.. -venv/bin/pytest diff --git a/downstream/python-lsp-server.sh b/downstream/python-lsp-server.sh deleted file mode 100644 index 0828faed..00000000 --- a/downstream/python-lsp-server.sh +++ /dev/null @@ -1,31 +0,0 @@ -set -eux -o pipefail -if [[ ! -d python-lsp-server ]]; then - git clone https://github.com/python-lsp/python-lsp-server.git -fi - -pushd python-lsp-server -trap popd EXIT - -git pull - -python -m venv venv - -if [[ "$OS" == "Windows_NT" ]]; then - VENV_PYTHON="venv/Scripts/python" - VENV_PYTEST="venv/Scripts/pytest" -else - VENV_PYTHON="venv/bin/python" - VENV_PYTEST="venv/bin/pytest" -fi - -# upgrade pip safely -"$VENV_PYTHON" -m pip install -U pip - -# install python-lsp-server test deps -"$VENV_PYTHON" -m pip install -e .[test] - -# install local pluggy -"$VENV_PYTHON" -m pip install -e .. - -# run tests -"$VENV_PYTEST" || true diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml new file mode 100644 index 00000000..dd1c4418 --- /dev/null +++ b/downstream/recipes/conda.toml @@ -0,0 +1,19 @@ +[git] +url = "https://github.com/conda/conda" +into = "conda" + +[environment] +kind = "none" + +[environment.install] +editables = ["../.."] + +[environment.install.bootstrap] +source = "dev/start" + +[[test]] +argv = [ + "bash", + "-c", + "set +eu; source dev/start; set -eu; pytest -m \"not integration and not installed\"", +] diff --git a/downstream/recipes/datasette.toml b/downstream/recipes/datasette.toml new file mode 100644 index 00000000..977a7303 --- /dev/null +++ b/downstream/recipes/datasette.toml @@ -0,0 +1,15 @@ +[git] +url = "https://github.com/simonw/datasette" +into = "datasette" + +[environment] +kind = "uv-venv" + +[environment.install] +editables = [".", "../.."] + +[environment.install.uv] +groups = ["dev"] + +[[test]] +argv = ["pytest"] diff --git a/downstream/recipes/devpi.toml b/downstream/recipes/devpi.toml new file mode 100644 index 00000000..ad1855c4 --- /dev/null +++ b/downstream/recipes/devpi.toml @@ -0,0 +1,24 @@ +[git] +url = "https://github.com/devpi/devpi" +into = "devpi" + +[environment] +kind = "uv-venv" + +[environment.install] +editables = ["common", "server", "client", "web", "../.."] + +[environment.install.uv] +groups = ["pytest"] + +[[test]] +argv = ["pytest", "common"] + +[[test]] +argv = ["pytest", "server"] + +[[test]] +argv = ["pytest", "client"] + +[[test]] +argv = ["pytest", "web"] diff --git a/downstream/recipes/hatch.toml b/downstream/recipes/hatch.toml new file mode 100644 index 00000000..d76ac699 --- /dev/null +++ b/downstream/recipes/hatch.toml @@ -0,0 +1,22 @@ +[git] +url = "https://github.com/pypa/hatch" +into = "hatch" + +[environment] +kind = "uv-venv" + +[environment.install] +editables = [".", "./backend", "../.."] + +[environment.install.uv] +packages = [ + "pytest", + "pytest-xdist", + "filelock", + "flit-core", + "trustme", + "editables", +] + +[[test]] +argv = ["pytest", "tests/backend"] diff --git a/downstream/recipes/pytest.toml b/downstream/recipes/pytest.toml new file mode 100644 index 00000000..00e7b48a --- /dev/null +++ b/downstream/recipes/pytest.toml @@ -0,0 +1,13 @@ +[git] +url = "https://github.com/pytest-dev/pytest" +into = "pytest" +shallow = false + +[environment] +kind = "stdlib-venv" + +[environment.install] +editables = [".[dev]", "../.."] + +[[test]] +argv = ["pytest"] diff --git a/downstream/recipes/python-lsp-server.toml b/downstream/recipes/python-lsp-server.toml new file mode 100644 index 00000000..ea9e515f --- /dev/null +++ b/downstream/recipes/python-lsp-server.toml @@ -0,0 +1,12 @@ +[git] +url = "https://github.com/python-lsp/python-lsp-server.git" +into = "python-lsp-server" + +[environment] +kind = "uv-venv" + +[environment.install] +editables = [".[all,test]", "../.."] + +[[test]] +argv = ["pytest"] diff --git a/downstream/recipes/tox.toml b/downstream/recipes/tox.toml new file mode 100644 index 00000000..7de9a5bf --- /dev/null +++ b/downstream/recipes/tox.toml @@ -0,0 +1,15 @@ +[git] +url = "https://github.com/tox-dev/tox" +into = "tox" + +[environment] +kind = "uv-venv" + +[environment.install] +editables = [".[completion]", "../.."] + +[environment.install.uv] +groups = ["test"] + +[[test]] +argv = ["pytest"] diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py new file mode 100644 index 00000000..8f7a3dda --- /dev/null +++ b/downstream/run_downstream.py @@ -0,0 +1,372 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "pydantic>=2.7", +# ] +# /// +from __future__ import annotations + +import argparse +from collections.abc import Mapping +from collections.abc import Sequence +import os +from pathlib import Path +import shlex +import subprocess +import sys +from typing import Annotated +from typing import Literal + +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import Field +from pydantic import field_validator +from pydantic import ValidationError +import tomllib + + +class GitConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + url: str + into: str + shallow: bool = True + + +class UvInstallOptions(BaseModel): + model_config = ConfigDict(extra="forbid") + + groups: list[str] = Field(default_factory=list) + packages: list[str] = Field(default_factory=list) + + +class PipInstallOptions(BaseModel): + model_config = ConfigDict(extra="forbid") + + packages: list[str] = Field(default_factory=list) + + +class BootstrapConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + source: str + + +class _InstallEditables(BaseModel): + model_config = ConfigDict(extra="forbid") + + editables: list[str] = Field(min_length=1) + + @field_validator("editables") + @classmethod + def editables_non_empty_strings(cls, v: list[str]) -> list[str]: + for i, s in enumerate(v): + if not s.strip(): + msg = f"editables[{i}] must be a non-empty string" + raise ValueError(msg) + return v + + +class UvInstall(_InstallEditables): + uv: UvInstallOptions | None = None + + +class StdlibInstall(_InstallEditables): + pip: PipInstallOptions | None = None + + +class NoneInstall(_InstallEditables): + bootstrap: BootstrapConfig + + +class EnvironmentUv(BaseModel): + model_config = ConfigDict(extra="forbid") + + kind: Literal["uv-venv"] + install: UvInstall + + +class EnvironmentStdlib(BaseModel): + model_config = ConfigDict(extra="forbid") + + kind: Literal["stdlib-venv"] + install: StdlibInstall + + +class EnvironmentNone(BaseModel): + model_config = ConfigDict(extra="forbid") + + kind: Literal["none"] + install: NoneInstall + + +Environment = Annotated[ + EnvironmentUv | EnvironmentStdlib | EnvironmentNone, + Field(discriminator="kind"), +] + + +class TestStep(BaseModel): + model_config = ConfigDict(extra="forbid") + + argv: list[str] = Field(min_length=1) + + +class RecipeFile(BaseModel): + model_config = ConfigDict(extra="forbid") + + git: GitConfig + environment: Environment + test: list[TestStep] = Field(min_length=1) + + +DOWNSTREAM_DIR = Path(__file__).resolve().parent +RECIPES_DIR = DOWNSTREAM_DIR / "recipes" + +# Local venv layout is fixed (simplifies recipes). +VENV_DIRNAME = "venv" +PYTHONBIN_FOR_STDLIB_VENV_CREATE = "python" + + +def venv_bin_dir(venv_home: Path) -> Path: + root = venv_home.resolve() + posix = root / "bin" + if posix.is_dir(): + return posix + return root / "Scripts" + + +def venv_python(venv_home: Path) -> Path: + d = venv_bin_dir(venv_home) + for name in ("python", "python3", "python.exe"): + candidate = d / name + if candidate.is_file(): + return candidate + return d / "python" + + +def venv_pip(venv_home: Path) -> Path: + d = venv_bin_dir(venv_home) + for name in ("pip", "pip.exe"): + candidate = d / name + if candidate.is_file(): + return candidate + return d / "pip" + + +def subprocess_env( + *, + extra: Mapping[str, str] | None, + venv_home: Path | None, +) -> dict[str, str]: + env = {**os.environ, **dict(extra or {})} + if venv_home is None: + return env + root = venv_home.resolve() + bindir = venv_bin_dir(root) + env["VIRTUAL_ENV"] = str(root) + env["PATH"] = str(bindir) + os.pathsep + env.get("PATH", "") + return env + + +def echo_cmd(argv: Sequence[str], *, cwd: Path) -> None: + display = shlex.join(argv) + print(f"+ cd {cwd.as_posix()} && {display}", flush=True) + + +def run_cmd( + argv: Sequence[str], + *, + cwd: Path, + env: Mapping[str, str] | None = None, + venv_home: Path | None = None, +) -> None: + echo_cmd(argv, cwd=cwd) + merged_env = subprocess_env(extra=env, venv_home=venv_home) + result = subprocess.run( + list(argv), + cwd=cwd, + env=merged_env, + check=False, + ) + if result.returncode != 0: + sys.exit(result.returncode) + + +def git_clone_or_pull(*, dest: Path, url: str, shallow: bool) -> None: + if dest.is_dir(): + run_cmd(["git", "-C", str(dest), "pull", "--ff-only"], cwd=dest.parent) + return + args = ["git", "clone"] + if shallow: + args.extend(["--depth", "1"]) + args.extend([url, str(dest)]) + run_cmd(args, cwd=dest.parent) + + +def venv_pyvenv_cfg(root: Path) -> Path: + return root / VENV_DIRNAME / "pyvenv.cfg" + + +def ensure_environment(*, root: Path, environment: Environment) -> None: + if isinstance(environment, EnvironmentNone): + return + if venv_pyvenv_cfg(root).is_file(): + return + if isinstance(environment, EnvironmentUv): + run_cmd(["uv", "venv", VENV_DIRNAME], cwd=root) + else: + run_cmd( + [PYTHONBIN_FOR_STDLIB_VENV_CREATE, "-m", "venv", VENV_DIRNAME], + cwd=root, + ) + + +def format_validation_error(path_name: str, err: ValidationError) -> str: + lines = [f"{path_name}: recipe validation failed"] + for e in err.errors(): + loc = ".".join(str(x) for x in e["loc"]) + msg = e["msg"] + lines.append(f" {loc}: {msg}") + return "\n".join(lines) + + +def load_recipe(name: str) -> RecipeFile: + path = RECIPES_DIR / f"{name}.toml" + if not path.is_file(): + available = ", ".join(sorted(p.stem for p in RECIPES_DIR.glob("*.toml"))) + print(f"Unknown downstream {name!r}. Available: {available}", file=sys.stderr) + sys.exit(2) + with path.open("rb") as fh: + data = tomllib.load(fh) + try: + return RecipeFile.model_validate(data) + except ValidationError as e: + print(format_validation_error(path.name, e), file=sys.stderr) + sys.exit(2) + + +def build_uv_install_argv(*, venv_home: Path, install: UvInstall) -> list[str]: + py = str(venv_python(venv_home)) + args: list[str] = ["uv", "pip", "install", "--python", py] + uv = install.uv + if uv is not None: + for g in uv.groups: + args.extend(["--group", g]) + for spec in install.editables: + args.extend(["-e", spec]) + if uv is not None: + for pkg in uv.packages: + args.append(pkg) + return args + + +def build_stdlib_install_argv(*, venv_home: Path, install: StdlibInstall) -> list[str]: + pip_exe = str(venv_pip(venv_home)) + args: list[str] = [pip_exe, "install"] + for spec in install.editables: + args.extend(["-e", spec]) + pip_extra = install.pip + if pip_extra is not None: + for pkg in pip_extra.packages: + args.append(pkg) + return args + + +def build_bootstrap_install_argv(*, install: NoneInstall) -> list[str]: + boot = install.bootstrap + inner_pip: list[str] = ["pip", "install"] + for spec in install.editables: + inner_pip.extend(["-e", spec]) + script = ( + f"set +eu; source {shlex.quote(boot.source)}; set -eu; {shlex.join(inner_pip)}" + ) + return ["bash", "-c", script] + + +def run_recipe( + name: str, + *, + skip_install: bool = False, + only_install: bool = False, +) -> None: + recipe = load_recipe(name) + dest = DOWNSTREAM_DIR / recipe.git.into + git_clone_or_pull(dest=dest, url=recipe.git.url, shallow=recipe.git.shallow) + profile = recipe.environment + ensure_environment(root=dest, environment=profile) + if isinstance(profile, EnvironmentNone): + venv_home = None + else: + venv_home = dest / VENV_DIRNAME + if not skip_install: + if isinstance(profile, EnvironmentUv): + argv_i = build_uv_install_argv( + venv_home=venv_home, + install=profile.install, + ) + elif isinstance(profile, EnvironmentStdlib): + argv_i = build_stdlib_install_argv( + venv_home=venv_home, + install=profile.install, + ) + else: + argv_i = build_bootstrap_install_argv(install=profile.install) + run_cmd(argv_i, cwd=dest, venv_home=venv_home) + if not only_install: + for step in recipe.test: + run_cmd(list(step.argv), cwd=dest, venv_home=venv_home) + + +def list_recipes() -> None: + names = sorted(p.stem for p in RECIPES_DIR.glob("*.toml")) + for n in names: + print(n) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Clone or update a downstream project and run its check recipe.", + ) + parser.add_argument( + "downstream", + nargs="?", + help="Recipe name (see *.toml in downstream/recipes/)", + ) + parser.add_argument( + "--list", + action="store_true", + help="Print available recipe names and exit.", + ) + parser.add_argument( + "--skip-install", + action="store_true", + help="Run environment + tests only (reuse existing installs).", + ) + parser.add_argument( + "--only-install", + action="store_true", + help="Run environment + install phases only (skip tests).", + ) + return parser + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + args = parser.parse_args(argv if argv is not None else sys.argv[1:]) + if args.list: + list_recipes() + return + if not args.downstream: + parser.error("downstream recipe name is required (or use --list)") + if args.only_install and args.skip_install: + parser.error("--only-install and --skip-install are mutually exclusive") + run_recipe( + args.downstream, + skip_install=args.skip_install, + only_install=args.only_install, + ) + + +if __name__ == "__main__": + main() diff --git a/downstream/tox.sh b/downstream/tox.sh deleted file mode 100755 index 79e12dfa..00000000 --- a/downstream/tox.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -eux -o pipefail -if [[ ! -d tox ]]; then - git clone https://github.com/tox-dev/tox -fi -pushd tox && trap popd EXIT -python -m venv venv -venv/bin/pip install -e .[testing] -e ../.. -venv/bin/pytest From df2257b0ac6574bec9960beee451a3c6e2a2fa46 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 6 May 2026 11:44:40 +0200 Subject: [PATCH 02/17] ci: add manual downstream workflow for recipe matrix Introduce a workflow_dispatch-only workflow that runs each downstream TOML recipe (conda, datasette, devpi, hatch, pytest, python-lsp-server, tox) on ubuntu-latest with Python 3.12 and uv. Matrix jobs are independent (fail-fast: false) and time out after 120 minutes. Trigger from Actions and, when validating a change that needs it, select the relevant branch under "Use workflow from" (for example a pull request head branch). Co-authored-by: Cursor AI Co-authored-by: Composer --- .github/workflows/downstream.yml | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/downstream.yml diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml new file mode 100644 index 00000000..21a3d5a2 --- /dev/null +++ b/.github/workflows/downstream.yml @@ -0,0 +1,36 @@ +# Downstream checks are manual-only. For a PR: Actions → downstream → Run workflow, +# then pick the PR branch under "Use workflow from". +name: downstream + +on: + workflow_dispatch: + +permissions: {} + +jobs: + recipe: + name: downstream (${{ matrix.recipe }}) + runs-on: ubuntu-latest + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + recipe: + - conda + - datasette + - devpi + - hatch + - pytest + - python-lsp-server + - tox + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + allow-prereleases: true + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - name: Run downstream recipe + run: uv run downstream/run_downstream.py "${{ matrix.recipe }}" From 9b120a95789360234f35db70460d94ce79a116f3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 6 May 2026 11:55:41 +0200 Subject: [PATCH 03/17] ci: run downstream workflow when PR has run-downstream label workflow_dispatch alone does not show checks on a pull request; add a label-gated pull_request trigger (labeled, synchronize) so maintainers can apply run-downstream to opt in. Document that the workflow file must exist on the default branch to appear under upstream Actions. Co-authored-by: Cursor AI Co-authored-by: Composer --- .github/workflows/downstream.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 21a3d5a2..e31157e2 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -1,17 +1,39 @@ -# Downstream checks are manual-only. For a PR: Actions → downstream → Run workflow, -# then pick the PR branch under "Use workflow from". +# Downstream checks are opt-in on pull requests (label: run-downstream) or fully +# manual via workflow_dispatch. For dispatch-only runs: Actions → downstream → Run +# workflow → pick a branch under "Use workflow from". +# +# The workflow must exist on the default branch for it to appear under the +# upstream repo's Actions tab; until the workflow is merged, use the fork's +# Actions (or dispatch from a branch that contains this file). name: downstream on: workflow_dispatch: + pull_request: + types: [labeled, synchronize] permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: recipe: name: downstream (${{ matrix.recipe }}) runs-on: ubuntu-latest timeout-minutes: 120 + # No default runs: add label "run-downstream" to the PR, or use workflow_dispatch. + if: >- + github.event_name == 'workflow_dispatch' + || ( + github.event_name == 'pull_request' + && contains(github.event.pull_request.labels.*.name, 'run-downstream') + && ( + github.event.action == 'synchronize' + || (github.event.action == 'labeled' && github.event.label.name == 'run-downstream') + ) + ) strategy: fail-fast: false matrix: From 82b714ce9f90fd2e4077974a1bb5fc06c0eb61b3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 6 May 2026 14:19:16 +0200 Subject: [PATCH 04/17] ci: gate downstream PR runs on branch name containing downstream Replace the run-downstream label with a head branch filter so forks without shared labels still opt in (e.g. downstream-driver). Pull requests still target main only. Co-authored-by: Cursor AI Co-authored-by: Composer --- .github/workflows/downstream.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index e31157e2..a4c699a8 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -1,6 +1,6 @@ -# Downstream checks are opt-in on pull requests (label: run-downstream) or fully -# manual via workflow_dispatch. For dispatch-only runs: Actions → downstream → Run -# workflow → pick a branch under "Use workflow from". +# Downstream checks run on pull requests to main when the head branch name +# contains "downstream", or any time via workflow_dispatch. For dispatch-only +# runs: Actions → downstream → Run workflow → pick a branch under "Use workflow from". # # The workflow must exist on the default branch for it to appear under the # upstream repo's Actions tab; until the workflow is merged, use the fork's @@ -10,7 +10,8 @@ name: downstream on: workflow_dispatch: pull_request: - types: [labeled, synchronize] + branches: + - main permissions: {} @@ -23,17 +24,9 @@ jobs: name: downstream (${{ matrix.recipe }}) runs-on: ubuntu-latest timeout-minutes: 120 - # No default runs: add label "run-downstream" to the PR, or use workflow_dispatch. if: >- github.event_name == 'workflow_dispatch' - || ( - github.event_name == 'pull_request' - && contains(github.event.pull_request.labels.*.name, 'run-downstream') - && ( - github.event.action == 'synchronize' - || (github.event.action == 'labeled' && github.event.label.name == 'run-downstream') - ) - ) + || (github.event_name == 'pull_request' && contains(github.head_ref, 'downstream')) strategy: fail-fast: false matrix: From 895d3fe22f83711aed30e00db500dfec9e41cbc1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 6 May 2026 14:22:53 +0200 Subject: [PATCH 05/17] ci: use full git history for downstream workflow checkout actions/checkout defaults to a shallow clone; pluggy installs as -e ../.. need a real tree (e.g. setuptools-scm). Set fetch-depth: 0. Co-authored-by: Cursor AI Co-authored-by: Composer --- .github/workflows/downstream.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index a4c699a8..9b095bbc 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -41,6 +41,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: From f8fb1ee39494a2ab48b579047beaad729a2f19fd Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 10 May 2026 08:52:28 +0200 Subject: [PATCH 06/17] fix(downstream): install pytest-mock for hatch recipe Hatch's backend tests use the pytest-mock "mocker" fixture; include it in the extra uv packages so downstream CI matches a full dev install. Co-authored-by: Cursor AI Co-authored-by: Composer --- downstream/recipes/hatch.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/downstream/recipes/hatch.toml b/downstream/recipes/hatch.toml index d76ac699..715e6905 100644 --- a/downstream/recipes/hatch.toml +++ b/downstream/recipes/hatch.toml @@ -11,6 +11,7 @@ editables = [".", "./backend", "../.."] [environment.install.uv] packages = [ "pytest", + "pytest-mock", "pytest-xdist", "filelock", "flit-core", From b806ff939278d45ca5d305e6ba3f1c26c6e8809b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 10 May 2026 10:03:41 +0200 Subject: [PATCH 07/17] fix(downstream): resolve env setup issues in downstream recipes - Add env field to TestStep for per-step environment overrides - tox: full clone (fixes 0.1.dev1 version) + CI=false (suppresses list_dependencies/freeze steps that break test expectations) - conda: full clone (fixes vcs_versioning shallow warnings) + CONDA_CHANNELS=defaults,conda-forge for channel-dependent tests - devpi: deselect test_upload.py (upstream conftest bug comparing list code=[200,200,200] against integers) - python-lsp-server: deselect 2 jedi tests needing /tmp/pyenv/ Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/README.md | 2 ++ downstream/recipes/conda.toml | 6 +++++- downstream/recipes/devpi.toml | 4 +++- downstream/recipes/python-lsp-server.toml | 8 +++++++- downstream/recipes/tox.toml | 4 ++++ downstream/run_downstream.py | 8 +++++++- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/downstream/README.md b/downstream/README.md index 68c3721a..8f6e3799 100644 --- a/downstream/README.md +++ b/downstream/README.md @@ -22,6 +22,8 @@ sections plus tests: 3. **`[[test]]`** — one or more `argv` arrays. For `uv-venv` / `stdlib-venv`, the driver sets **`VIRTUAL_ENV`** and prepends the venv’s `bin` (or `Scripts` on Windows) to **`PATH`**, so test commands can use bare names like `pytest`. + Optional **`env`** table sets extra environment variables for the step (e.g. + `env = { CI = "false" }` to suppress CI-only behaviour in a downstream suite). Install only: `--only-install`. Run the driver (PEP 723 in `run_downstream.py`): diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index dd1c4418..d9f442d5 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -1,6 +1,7 @@ [git] url = "https://github.com/conda/conda" into = "conda" +shallow = false [environment] kind = "none" @@ -15,5 +16,8 @@ source = "dev/start" argv = [ "bash", "-c", - "set +eu; source dev/start; set -eu; pytest -m \"not integration and not installed\"", + "set +eu; source dev/start; set -eu; pytest -m 'not integration and not installed'", ] +# Many conda tests create temporary envs that need channels to resolve +# packages; setting CONDA_CHANNELS provides a sensible default on CI. +env = { CONDA_CHANNELS = "defaults,conda-forge" } diff --git a/downstream/recipes/devpi.toml b/downstream/recipes/devpi.toml index ad1855c4..a4106765 100644 --- a/downstream/recipes/devpi.toml +++ b/downstream/recipes/devpi.toml @@ -18,7 +18,9 @@ argv = ["pytest", "common"] argv = ["pytest", "server"] [[test]] -argv = ["pytest", "client"] +# Deselect test_upload: upstream conftest bug compares code=[200,200,200] +# against integers, causing TypeError on every upload-with-docs test. +argv = ["pytest", "client", "--deselect=client/testing/test_upload.py"] [[test]] argv = ["pytest", "web"] diff --git a/downstream/recipes/python-lsp-server.toml b/downstream/recipes/python-lsp-server.toml index ea9e515f..2aa70870 100644 --- a/downstream/recipes/python-lsp-server.toml +++ b/downstream/recipes/python-lsp-server.toml @@ -9,4 +9,10 @@ kind = "uv-venv" editables = [".[all,test]", "../.."] [[test]] -argv = ["pytest"] +# Deselect two jedi-environment tests that require /tmp/pyenv/ fixture +# not provided by the default test environment. +argv = [ + "pytest", + "--deselect=test/plugins/test_completion.py::test_jedi_completion_environment", + "--deselect=test/plugins/test_symbols.py::test_symbols_all_scopes_with_jedi_environment", +] diff --git a/downstream/recipes/tox.toml b/downstream/recipes/tox.toml index 7de9a5bf..694eecbb 100644 --- a/downstream/recipes/tox.toml +++ b/downstream/recipes/tox.toml @@ -1,6 +1,7 @@ [git] url = "https://github.com/tox-dev/tox" into = "tox" +shallow = false [environment] kind = "uv-venv" @@ -13,3 +14,6 @@ groups = ["test"] [[test]] argv = ["pytest"] +# tox detects CI=true and enables list_dependencies by default, inserting +# extra "freeze" steps that its own test expectations don't account for. +env = { CI = "false" } diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index 8f7a3dda..4019cbe0 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -110,6 +110,7 @@ class TestStep(BaseModel): model_config = ConfigDict(extra="forbid") argv: list[str] = Field(min_length=1) + env: dict[str, str] = Field(default_factory=dict) class RecipeFile(BaseModel): @@ -315,7 +316,12 @@ def run_recipe( run_cmd(argv_i, cwd=dest, venv_home=venv_home) if not only_install: for step in recipe.test: - run_cmd(list(step.argv), cwd=dest, venv_home=venv_home) + run_cmd( + list(step.argv), + cwd=dest, + env=step.env or None, + venv_home=venv_home, + ) def list_recipes() -> None: From 9102a492997f67fa3034a7bdc0e2388956de56ec Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 11 May 2026 22:08:54 +0200 Subject: [PATCH 08/17] fix(downstream): fix tox CI detection, devpi deselect, drop conda env hack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tox: also override GITHUB_ACTIONS=false (is_ci() checks both CI and GITHUB_ACTIONS env vars) - devpi: switch --deselect to --ignore for test_upload.py (deselect path didn't match pytest node IDs) - conda: remove CONDA_CHANNELS override — it fixes NoChannels tests but breaks channel-configuration tests; remaining failures are upstream conda CI infrastructure issues Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/recipes/conda.toml | 3 --- downstream/recipes/devpi.toml | 4 ++-- downstream/recipes/tox.toml | 7 ++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index d9f442d5..f3c7e774 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -18,6 +18,3 @@ argv = [ "-c", "set +eu; source dev/start; set -eu; pytest -m 'not integration and not installed'", ] -# Many conda tests create temporary envs that need channels to resolve -# packages; setting CONDA_CHANNELS provides a sensible default on CI. -env = { CONDA_CHANNELS = "defaults,conda-forge" } diff --git a/downstream/recipes/devpi.toml b/downstream/recipes/devpi.toml index a4106765..ca1fb884 100644 --- a/downstream/recipes/devpi.toml +++ b/downstream/recipes/devpi.toml @@ -18,9 +18,9 @@ argv = ["pytest", "common"] argv = ["pytest", "server"] [[test]] -# Deselect test_upload: upstream conftest bug compares code=[200,200,200] +# Ignore test_upload: upstream conftest bug compares code=[200,200,200] # against integers, causing TypeError on every upload-with-docs test. -argv = ["pytest", "client", "--deselect=client/testing/test_upload.py"] +argv = ["pytest", "client", "--ignore=client/testing/test_upload.py"] [[test]] argv = ["pytest", "web"] diff --git a/downstream/recipes/tox.toml b/downstream/recipes/tox.toml index 694eecbb..650ebce4 100644 --- a/downstream/recipes/tox.toml +++ b/downstream/recipes/tox.toml @@ -14,6 +14,7 @@ groups = ["test"] [[test]] argv = ["pytest"] -# tox detects CI=true and enables list_dependencies by default, inserting -# extra "freeze" steps that its own test expectations don't account for. -env = { CI = "false" } +# tox's is_ci() checks CI, GITHUB_ACTIONS, and many other env vars; +# all must be unset/false so list_dependencies defaults to False and +# test expectations (no freeze steps, spinner enabled, etc.) hold. +env = { CI = "false", GITHUB_ACTIONS = "false" } From 198eb6aba335adf79c4ffe7c51669783cff5ea0a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 09:31:17 +0200 Subject: [PATCH 09/17] fix(downstream): remove CI env vars for tox instead of overriding tox's is_ci() checks presence of CI (any value) and GITHUB_ACTIONS==true. Setting CI=false still leaves it present, so is_ci() returns True. Now empty-string env values mean "remove from environment" in the driver, and tox recipe uses CI="" and GITHUB_ACTIONS="" to fully unset them. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/README.md | 5 +++-- downstream/recipes/tox.toml | 9 +++++---- downstream/run_downstream.py | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/downstream/README.md b/downstream/README.md index 8f6e3799..839203e5 100644 --- a/downstream/README.md +++ b/downstream/README.md @@ -22,8 +22,9 @@ sections plus tests: 3. **`[[test]]`** — one or more `argv` arrays. For `uv-venv` / `stdlib-venv`, the driver sets **`VIRTUAL_ENV`** and prepends the venv’s `bin` (or `Scripts` on Windows) to **`PATH`**, so test commands can use bare names like `pytest`. - Optional **`env`** table sets extra environment variables for the step (e.g. - `env = { CI = "false" }` to suppress CI-only behaviour in a downstream suite). + Optional **`env`** table sets extra environment variables for the step; + an empty string removes the variable from the environment (e.g. + `env = { CI = "" }` to unset `CI` and suppress CI-only behaviour). Install only: `--only-install`. Run the driver (PEP 723 in `run_downstream.py`): diff --git a/downstream/recipes/tox.toml b/downstream/recipes/tox.toml index 650ebce4..3ef150a2 100644 --- a/downstream/recipes/tox.toml +++ b/downstream/recipes/tox.toml @@ -14,7 +14,8 @@ groups = ["test"] [[test]] argv = ["pytest"] -# tox's is_ci() checks CI, GITHUB_ACTIONS, and many other env vars; -# all must be unset/false so list_dependencies defaults to False and -# test expectations (no freeze steps, spinner enabled, etc.) hold. -env = { CI = "false", GITHUB_ACTIONS = "false" } +# tox's is_ci() checks presence of CI, value of GITHUB_ACTIONS, and +# other env vars. Empty string = remove from environment, non-empty +# overrides value. Both must be gone for list_dependencies to default +# to False and test expectations (no freeze steps, etc.) to hold. +env = { CI = "", GITHUB_ACTIONS = "" } diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index 4019cbe0..b15b63ae 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -161,6 +161,10 @@ def subprocess_env( venv_home: Path | None, ) -> dict[str, str]: env = {**os.environ, **dict(extra or {})} + # Empty-string values mean "remove from environment". + for key, val in list(env.items()): + if val == "": + del env[key] if venv_home is None: return env root = venv_home.resolve() From e56d97709a1e0ceee5ec19cc978eef80d88becf7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 11:26:33 +0200 Subject: [PATCH 10/17] fix(downstream): configure conda channels via condarc for test suite conda's own CI uses condarc-file to set channels; dev/start alone doesn't write one, so tests creating temporary envs fail with NoChannelsConfiguredError. Add `conda config --add channels defaults` after bootstrap, matching their CI's condarc-defaults configuration. Unlike the CONDA_CHANNELS env var (which conflicted with channel-config tests), writing to .condarc is the proper mechanism that conda's test fixtures handle correctly. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/recipes/conda.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index f3c7e774..dacda7d6 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -13,8 +13,11 @@ editables = ["../.."] source = "dev/start" [[test]] +# conda's own CI uses condarc-file to configure channels; dev/start +# alone doesn't write one, so tests that create temporary envs fail +# with NoChannelsConfiguredError. Mirror their CI config here. argv = [ "bash", "-c", - "set +eu; source dev/start; set -eu; pytest -m 'not integration and not installed'", + "set +eu; source dev/start; set -eu; conda config --add channels defaults; pytest -m 'not integration and not installed'", ] From f418924fc5f19609d45d912e11443e97fcdc0b12 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 13:49:18 +0200 Subject: [PATCH 11/17] fix(downstream): deselect conda export-from-history test test_export_from_history_format fails because our pluggy is pip-installed (editable) into the conda env rather than conda-installed, so it's missing from conda's explicit_packages. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/recipes/conda.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index dacda7d6..77932a35 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -16,8 +16,10 @@ source = "dev/start" # conda's own CI uses condarc-file to configure channels; dev/start # alone doesn't write one, so tests that create temporary envs fail # with NoChannelsConfiguredError. Mirror their CI config here. +# test_export_from_history_format fails because our pluggy is pip-installed +# (editable) rather than conda-installed, breaking history-based export. argv = [ "bash", "-c", - "set +eu; source dev/start; set -eu; conda config --add channels defaults; pytest -m 'not integration and not installed'", + "set +eu; source dev/start; set -eu; conda config --add channels defaults; pytest -m 'not integration and not installed' --deselect=tests/cli/test_main_export.py::test_export_from_history_format", ] From b8fceda01a5e5879dfeead83a526c890f47095bd Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 14:42:59 +0200 Subject: [PATCH 12/17] refactor(downstream): simplify to uv-venv and script env kinds Drop stdlib-venv and EnvironmentNone/bootstrap in favour of two environment kinds: uv-venv (venv + uv pip install) and script (self-contained bash script handling the full workflow). - Move conda's inline bash into recipes/conda.bash - Switch pytest recipe from stdlib-venv to uv-venv - Remove PipInstallOptions, StdlibInstall, NoneInstall, BootstrapConfig and related helpers (-104 lines of Python) Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/README.md | 40 +++++------ downstream/recipes/conda.bash | 22 ++++++ downstream/recipes/conda.toml | 21 +----- downstream/recipes/pytest.toml | 2 +- downstream/run_downstream.py | 127 +++++++-------------------------- 5 files changed, 65 insertions(+), 147 deletions(-) create mode 100755 downstream/recipes/conda.bash diff --git a/downstream/README.md b/downstream/README.md index 839203e5..1e18a7e7 100644 --- a/downstream/README.md +++ b/downstream/README.md @@ -2,30 +2,24 @@ This directory contains tooling for testing some downstream projects against your current pluggy checkout. Each project is described by a TOML recipe in `recipes/` with three top-level -sections plus tests: +sections: 1. **`[git]`** — repository URL, local directory name (`into`), optional `shallow` (default: shallow clone). -2. **`[environment]`** — how the Python environment is created **once** (skipped - if `venv/pyvenv.cfg` already exists): - - `kind = "uv-venv"` — `uv venv venv` in the clone root. - - `kind = "stdlib-venv"` — `python -m venv venv` in the clone root. - - `kind = "none"` — do not create a venv (e.g. conda’s `dev/start` workflow). - **`[environment.install]`** holds **`editables`**: strings passed as consecutive - **`-e`** arguments to `uv pip install` or `pip install` (same forms you would - type on the command line, e.g. `".[dev]"`, `"../.."`). Optional - **`[environment.install.uv]`** (`groups`, `packages`) or - **`[environment.install.pip]`** (`packages`) match the environment kind. For - `kind = "none"`, **`[environment.install.bootstrap]`** with `source` is - required (shell script sourced before pip). Rerun tests without reinstall: - `uv run downstream/run_downstream.py --skip-install RECIPE`. -3. **`[[test]]`** — one or more `argv` arrays. For `uv-venv` / `stdlib-venv`, the - driver sets **`VIRTUAL_ENV`** and prepends the venv’s `bin` (or `Scripts` on - Windows) to **`PATH`**, so test commands can use bare names like `pytest`. - Optional **`env`** table sets extra environment variables for the step; - an empty string removes the variable from the environment (e.g. - `env = { CI = "" }` to unset `CI` and suppress CI-only behaviour). - Install only: `--only-install`. +2. **`[environment]`** — how the project is set up: + - `kind = "uv-venv"` — `uv venv venv` in the clone root, then install via + `uv pip install`. **`[environment.install]`** holds **`editables`**: strings + passed as `-e` arguments (e.g. `".[dev]"`, `"../.."`). Optional + **`[environment.install.uv]`** adds `groups` and `packages`. + - `kind = "script"` — delegate everything (bootstrap, install, test) to a + bash script via **`run`** (path relative to `downstream/`). No `[[test]]` + steps are used; the script handles the full workflow. +3. **`[[test]]`** (uv-venv only) — one or more `argv` arrays. The driver sets + **`VIRTUAL_ENV`** and prepends the venv's `bin` to **`PATH`**, so test + commands can use bare names like `pytest`. Optional **`env`** table sets + extra environment variables; an empty string removes the variable (e.g. + `env = { CI = "" }` to unset `CI`). + Install only: `--only-install`. Skip install: `--skip-install`. Run the driver (PEP 723 in `run_downstream.py`): @@ -35,6 +29,4 @@ uv run downstream/run_downstream.py pytest uv run downstream/run_downstream.py pytest --skip-install ``` -Requirements: Python 3.11+ for the driver, `git`, and `uv` where recipes use it. -Recipes using `uv-venv` / `stdlib-venv` get an activated-style environment for -install and test subprocesses; conda recipes use `bash` and their own `dev/start`. +Requirements: Python 3.11+ for the driver, `git`, and `uv`. diff --git a/downstream/recipes/conda.bash b/downstream/recipes/conda.bash new file mode 100755 index 00000000..99a731f4 --- /dev/null +++ b/downstream/recipes/conda.bash @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Bootstrap, install, and test conda against the local pluggy checkout. +# Called from run_downstream.py via the conda recipe. +set -eu + +cd "$(dirname "$0")/../conda" + +# dev/start needs relaxed error handling during bootstrap. +set +eu +source dev/start +set -eu + +# Install pluggy editable from the parent checkout. +pip install -e ../.. + +# Mirror conda's own CI condarc-defaults so tests that create temporary +# environments can resolve packages. +conda config --add channels defaults + +pytest \ + -m 'not integration and not installed' \ + --deselect=tests/cli/test_main_export.py::test_export_from_history_format diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index 77932a35..16499ddd 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -4,22 +4,5 @@ into = "conda" shallow = false [environment] -kind = "none" - -[environment.install] -editables = ["../.."] - -[environment.install.bootstrap] -source = "dev/start" - -[[test]] -# conda's own CI uses condarc-file to configure channels; dev/start -# alone doesn't write one, so tests that create temporary envs fail -# with NoChannelsConfiguredError. Mirror their CI config here. -# test_export_from_history_format fails because our pluggy is pip-installed -# (editable) rather than conda-installed, breaking history-based export. -argv = [ - "bash", - "-c", - "set +eu; source dev/start; set -eu; conda config --add channels defaults; pytest -m 'not integration and not installed' --deselect=tests/cli/test_main_export.py::test_export_from_history_format", -] +kind = "script" +run = "recipes/conda.bash" diff --git a/downstream/recipes/pytest.toml b/downstream/recipes/pytest.toml index 00e7b48a..cc870035 100644 --- a/downstream/recipes/pytest.toml +++ b/downstream/recipes/pytest.toml @@ -4,7 +4,7 @@ into = "pytest" shallow = false [environment] -kind = "stdlib-venv" +kind = "uv-venv" [environment.install] editables = [".[dev]", "../.."] diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index b15b63ae..e97cc744 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -40,22 +40,11 @@ class UvInstallOptions(BaseModel): packages: list[str] = Field(default_factory=list) -class PipInstallOptions(BaseModel): - model_config = ConfigDict(extra="forbid") - - packages: list[str] = Field(default_factory=list) - - -class BootstrapConfig(BaseModel): - model_config = ConfigDict(extra="forbid") - - source: str - - -class _InstallEditables(BaseModel): +class UvInstall(BaseModel): model_config = ConfigDict(extra="forbid") editables: list[str] = Field(min_length=1) + uv: UvInstallOptions | None = None @field_validator("editables") @classmethod @@ -67,18 +56,6 @@ def editables_non_empty_strings(cls, v: list[str]) -> list[str]: return v -class UvInstall(_InstallEditables): - uv: UvInstallOptions | None = None - - -class StdlibInstall(_InstallEditables): - pip: PipInstallOptions | None = None - - -class NoneInstall(_InstallEditables): - bootstrap: BootstrapConfig - - class EnvironmentUv(BaseModel): model_config = ConfigDict(extra="forbid") @@ -86,22 +63,15 @@ class EnvironmentUv(BaseModel): install: UvInstall -class EnvironmentStdlib(BaseModel): - model_config = ConfigDict(extra="forbid") - - kind: Literal["stdlib-venv"] - install: StdlibInstall - - -class EnvironmentNone(BaseModel): +class EnvironmentScript(BaseModel): model_config = ConfigDict(extra="forbid") - kind: Literal["none"] - install: NoneInstall + kind: Literal["script"] + run: str Environment = Annotated[ - EnvironmentUv | EnvironmentStdlib | EnvironmentNone, + EnvironmentUv | EnvironmentScript, Field(discriminator="kind"), ] @@ -118,15 +88,13 @@ class RecipeFile(BaseModel): git: GitConfig environment: Environment - test: list[TestStep] = Field(min_length=1) + test: list[TestStep] = Field(default_factory=list) DOWNSTREAM_DIR = Path(__file__).resolve().parent RECIPES_DIR = DOWNSTREAM_DIR / "recipes" -# Local venv layout is fixed (simplifies recipes). VENV_DIRNAME = "venv" -PYTHONBIN_FOR_STDLIB_VENV_CREATE = "python" def venv_bin_dir(venv_home: Path) -> Path: @@ -146,15 +114,6 @@ def venv_python(venv_home: Path) -> Path: return d / "python" -def venv_pip(venv_home: Path) -> Path: - d = venv_bin_dir(venv_home) - for name in ("pip", "pip.exe"): - candidate = d / name - if candidate.is_file(): - return candidate - return d / "pip" - - def subprocess_env( *, extra: Mapping[str, str] | None, @@ -209,22 +168,11 @@ def git_clone_or_pull(*, dest: Path, url: str, shallow: bool) -> None: run_cmd(args, cwd=dest.parent) -def venv_pyvenv_cfg(root: Path) -> Path: - return root / VENV_DIRNAME / "pyvenv.cfg" - - -def ensure_environment(*, root: Path, environment: Environment) -> None: - if isinstance(environment, EnvironmentNone): +def ensure_uv_venv(root: Path) -> None: + cfg = root / VENV_DIRNAME / "pyvenv.cfg" + if cfg.is_file(): return - if venv_pyvenv_cfg(root).is_file(): - return - if isinstance(environment, EnvironmentUv): - run_cmd(["uv", "venv", VENV_DIRNAME], cwd=root) - else: - run_cmd( - [PYTHONBIN_FOR_STDLIB_VENV_CREATE, "-m", "venv", VENV_DIRNAME], - cwd=root, - ) + run_cmd(["uv", "venv", VENV_DIRNAME], cwd=root) def format_validation_error(path_name: str, err: ValidationError) -> str: @@ -266,29 +214,6 @@ def build_uv_install_argv(*, venv_home: Path, install: UvInstall) -> list[str]: return args -def build_stdlib_install_argv(*, venv_home: Path, install: StdlibInstall) -> list[str]: - pip_exe = str(venv_pip(venv_home)) - args: list[str] = [pip_exe, "install"] - for spec in install.editables: - args.extend(["-e", spec]) - pip_extra = install.pip - if pip_extra is not None: - for pkg in pip_extra.packages: - args.append(pkg) - return args - - -def build_bootstrap_install_argv(*, install: NoneInstall) -> list[str]: - boot = install.bootstrap - inner_pip: list[str] = ["pip", "install"] - for spec in install.editables: - inner_pip.extend(["-e", spec]) - script = ( - f"set +eu; source {shlex.quote(boot.source)}; set -eu; {shlex.join(inner_pip)}" - ) - return ["bash", "-c", script] - - def run_recipe( name: str, *, @@ -298,26 +223,22 @@ def run_recipe( recipe = load_recipe(name) dest = DOWNSTREAM_DIR / recipe.git.into git_clone_or_pull(dest=dest, url=recipe.git.url, shallow=recipe.git.shallow) + profile = recipe.environment - ensure_environment(root=dest, environment=profile) - if isinstance(profile, EnvironmentNone): - venv_home = None - else: - venv_home = dest / VENV_DIRNAME + + if isinstance(profile, EnvironmentScript): + script = DOWNSTREAM_DIR / profile.run + run_cmd(["bash", str(script)], cwd=DOWNSTREAM_DIR) + return + + # uv-venv path + ensure_uv_venv(dest) + venv_home = dest / VENV_DIRNAME + if not skip_install: - if isinstance(profile, EnvironmentUv): - argv_i = build_uv_install_argv( - venv_home=venv_home, - install=profile.install, - ) - elif isinstance(profile, EnvironmentStdlib): - argv_i = build_stdlib_install_argv( - venv_home=venv_home, - install=profile.install, - ) - else: - argv_i = build_bootstrap_install_argv(install=profile.install) + argv_i = build_uv_install_argv(venv_home=venv_home, install=profile.install) run_cmd(argv_i, cwd=dest, venv_home=venv_home) + if not only_install: for step in recipe.test: run_cmd( From b6be9d907422621d878b09234146db9598ee3a59 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 15:00:02 +0200 Subject: [PATCH 13/17] refactor(downstream): flatten environment schema, distinguish by structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the `kind` discriminator field from environment configs. The two environment types are now distinguished by their keys: - `editables` present → uv-venv (creates venv, installs via uv pip) - `run` present → script (delegates to bash) Fold `[environment.install]` and `[environment.install.uv]` sub-tables directly into `[environment]`, removing UvInstall/UvInstallOptions models. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/README.md | 17 ++++---- downstream/recipes/conda.toml | 1 - downstream/recipes/datasette.toml | 5 --- downstream/recipes/devpi.toml | 5 --- downstream/recipes/hatch.toml | 5 --- downstream/recipes/pytest.toml | 3 -- downstream/recipes/python-lsp-server.toml | 3 -- downstream/recipes/tox.toml | 5 --- downstream/run_downstream.py | 50 ++++++++++------------- 9 files changed, 29 insertions(+), 65 deletions(-) diff --git a/downstream/README.md b/downstream/README.md index 1e18a7e7..30375e5b 100644 --- a/downstream/README.md +++ b/downstream/README.md @@ -1,19 +1,16 @@ This directory contains tooling for testing some downstream projects against your current pluggy checkout. -Each project is described by a TOML recipe in `recipes/` with three top-level -sections: +Each project is described by a TOML recipe in `recipes/` with three sections: 1. **`[git]`** — repository URL, local directory name (`into`), optional `shallow` (default: shallow clone). -2. **`[environment]`** — how the project is set up: - - `kind = "uv-venv"` — `uv venv venv` in the clone root, then install via - `uv pip install`. **`[environment.install]`** holds **`editables`**: strings - passed as `-e` arguments (e.g. `".[dev]"`, `"../.."`). Optional - **`[environment.install.uv]`** adds `groups` and `packages`. - - `kind = "script"` — delegate everything (bootstrap, install, test) to a - bash script via **`run`** (path relative to `downstream/`). No `[[test]]` - steps are used; the script handles the full workflow. +2. **`[environment]`** — distinguished by structure (no `kind` key needed): + - **uv-venv** (has `editables`): creates `uv venv`, installs via + `uv pip install`. `editables` are passed as `-e` args. Optional + `groups` and `packages` for extra dependencies. + - **script** (has `run`): delegates bootstrap, install, and test to a + bash script (path relative to `downstream/`). No `[[test]]` steps. 3. **`[[test]]`** (uv-venv only) — one or more `argv` arrays. The driver sets **`VIRTUAL_ENV`** and prepends the venv's `bin` to **`PATH`**, so test commands can use bare names like `pytest`. Optional **`env`** table sets diff --git a/downstream/recipes/conda.toml b/downstream/recipes/conda.toml index 16499ddd..326bd15e 100644 --- a/downstream/recipes/conda.toml +++ b/downstream/recipes/conda.toml @@ -4,5 +4,4 @@ into = "conda" shallow = false [environment] -kind = "script" run = "recipes/conda.bash" diff --git a/downstream/recipes/datasette.toml b/downstream/recipes/datasette.toml index 977a7303..30f19261 100644 --- a/downstream/recipes/datasette.toml +++ b/downstream/recipes/datasette.toml @@ -3,12 +3,7 @@ url = "https://github.com/simonw/datasette" into = "datasette" [environment] -kind = "uv-venv" - -[environment.install] editables = [".", "../.."] - -[environment.install.uv] groups = ["dev"] [[test]] diff --git a/downstream/recipes/devpi.toml b/downstream/recipes/devpi.toml index ca1fb884..8ba2efcf 100644 --- a/downstream/recipes/devpi.toml +++ b/downstream/recipes/devpi.toml @@ -3,12 +3,7 @@ url = "https://github.com/devpi/devpi" into = "devpi" [environment] -kind = "uv-venv" - -[environment.install] editables = ["common", "server", "client", "web", "../.."] - -[environment.install.uv] groups = ["pytest"] [[test]] diff --git a/downstream/recipes/hatch.toml b/downstream/recipes/hatch.toml index 715e6905..d0c2deb8 100644 --- a/downstream/recipes/hatch.toml +++ b/downstream/recipes/hatch.toml @@ -3,12 +3,7 @@ url = "https://github.com/pypa/hatch" into = "hatch" [environment] -kind = "uv-venv" - -[environment.install] editables = [".", "./backend", "../.."] - -[environment.install.uv] packages = [ "pytest", "pytest-mock", diff --git a/downstream/recipes/pytest.toml b/downstream/recipes/pytest.toml index cc870035..69664024 100644 --- a/downstream/recipes/pytest.toml +++ b/downstream/recipes/pytest.toml @@ -4,9 +4,6 @@ into = "pytest" shallow = false [environment] -kind = "uv-venv" - -[environment.install] editables = [".[dev]", "../.."] [[test]] diff --git a/downstream/recipes/python-lsp-server.toml b/downstream/recipes/python-lsp-server.toml index 2aa70870..da23579b 100644 --- a/downstream/recipes/python-lsp-server.toml +++ b/downstream/recipes/python-lsp-server.toml @@ -3,9 +3,6 @@ url = "https://github.com/python-lsp/python-lsp-server.git" into = "python-lsp-server" [environment] -kind = "uv-venv" - -[environment.install] editables = [".[all,test]", "../.."] [[test]] diff --git a/downstream/recipes/tox.toml b/downstream/recipes/tox.toml index 3ef150a2..a567c32a 100644 --- a/downstream/recipes/tox.toml +++ b/downstream/recipes/tox.toml @@ -4,12 +4,7 @@ into = "tox" shallow = false [environment] -kind = "uv-venv" - -[environment.install] editables = [".[completion]", "../.."] - -[environment.install.uv] groups = ["test"] [[test]] diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index e97cc744..cb554ade 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -15,12 +15,12 @@ import subprocess import sys from typing import Annotated -from typing import Literal from pydantic import BaseModel from pydantic import ConfigDict from pydantic import Field from pydantic import field_validator +from pydantic import model_validator from pydantic import ValidationError import tomllib @@ -33,18 +33,14 @@ class GitConfig(BaseModel): shallow: bool = True -class UvInstallOptions(BaseModel): - model_config = ConfigDict(extra="forbid") - - groups: list[str] = Field(default_factory=list) - packages: list[str] = Field(default_factory=list) - +class EnvironmentUv(BaseModel): + """uv-venv: create venv, install editables + optional groups/packages.""" -class UvInstall(BaseModel): model_config = ConfigDict(extra="forbid") editables: list[str] = Field(min_length=1) - uv: UvInstallOptions | None = None + groups: list[str] = Field(default_factory=list) + packages: list[str] = Field(default_factory=list) @field_validator("editables") @classmethod @@ -56,23 +52,17 @@ def editables_non_empty_strings(cls, v: list[str]) -> list[str]: return v -class EnvironmentUv(BaseModel): - model_config = ConfigDict(extra="forbid") - - kind: Literal["uv-venv"] - install: UvInstall - - class EnvironmentScript(BaseModel): + """script: delegate everything to a bash script.""" + model_config = ConfigDict(extra="forbid") - kind: Literal["script"] run: str Environment = Annotated[ EnvironmentUv | EnvironmentScript, - Field(discriminator="kind"), + Field(union_mode="left_to_right"), ] @@ -90,6 +80,13 @@ class RecipeFile(BaseModel): environment: Environment test: list[TestStep] = Field(default_factory=list) + @model_validator(mode="after") + def script_has_no_test_steps(self) -> RecipeFile: + if isinstance(self.environment, EnvironmentScript) and self.test: + msg = "script environments handle testing; [[test]] must be empty" + raise ValueError(msg) + return self + DOWNSTREAM_DIR = Path(__file__).resolve().parent RECIPES_DIR = DOWNSTREAM_DIR / "recipes" @@ -199,18 +196,15 @@ def load_recipe(name: str) -> RecipeFile: sys.exit(2) -def build_uv_install_argv(*, venv_home: Path, install: UvInstall) -> list[str]: +def build_uv_install_argv(*, venv_home: Path, env: EnvironmentUv) -> list[str]: py = str(venv_python(venv_home)) args: list[str] = ["uv", "pip", "install", "--python", py] - uv = install.uv - if uv is not None: - for g in uv.groups: - args.extend(["--group", g]) - for spec in install.editables: + for g in env.groups: + args.extend(["--group", g]) + for spec in env.editables: args.extend(["-e", spec]) - if uv is not None: - for pkg in uv.packages: - args.append(pkg) + for pkg in env.packages: + args.append(pkg) return args @@ -236,7 +230,7 @@ def run_recipe( venv_home = dest / VENV_DIRNAME if not skip_install: - argv_i = build_uv_install_argv(venv_home=venv_home, install=profile.install) + argv_i = build_uv_install_argv(venv_home=venv_home, env=profile) run_cmd(argv_i, cwd=dest, venv_home=venv_home) if not only_install: From fa2af2b84a3c164b7a16ac340119d42668a7da40 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 May 2026 15:09:40 +0200 Subject: [PATCH 14/17] cleanup(downstream): remove redundant type conversions in driver Drop unnecessary dict(), list(), and type annotations where the types are already correct or inferred from context. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/run_downstream.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index cb554ade..dc2b6498 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -116,9 +116,9 @@ def subprocess_env( extra: Mapping[str, str] | None, venv_home: Path | None, ) -> dict[str, str]: - env = {**os.environ, **dict(extra or {})} + env = {**os.environ, **(extra or {})} # Empty-string values mean "remove from environment". - for key, val in list(env.items()): + for key, val in env.items(): if val == "": del env[key] if venv_home is None: @@ -198,7 +198,7 @@ def load_recipe(name: str) -> RecipeFile: def build_uv_install_argv(*, venv_home: Path, env: EnvironmentUv) -> list[str]: py = str(venv_python(venv_home)) - args: list[str] = ["uv", "pip", "install", "--python", py] + args = ["uv", "pip", "install", "--python", py] for g in env.groups: args.extend(["--group", g]) for spec in env.editables: @@ -236,7 +236,7 @@ def run_recipe( if not only_install: for step in recipe.test: run_cmd( - list(step.argv), + step.argv, cwd=dest, env=step.env or None, venv_home=venv_home, From 40f3ab1293c48d2a149a5328be893dc3fa08b395 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 May 2026 20:38:59 +0200 Subject: [PATCH 15/17] fix(downstream): fix RuntimeError from mutating dict during iteration The previous cleanup accidentally dropped the list() wrapper around env.items(), causing a RuntimeError when empty-string values were deleted during iteration. Replace with a dict comprehension instead. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/run_downstream.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index dc2b6498..1bb91b27 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -118,9 +118,7 @@ def subprocess_env( ) -> dict[str, str]: env = {**os.environ, **(extra or {})} # Empty-string values mean "remove from environment". - for key, val in env.items(): - if val == "": - del env[key] + env = {k: v for k, v in env.items() if v != ""} if venv_home is None: return env root = venv_home.resolve() From 5b9a37cbf43fee0f3bbf8ed03586e5dd0695bf56 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 17 May 2026 08:07:18 +0200 Subject: [PATCH 16/17] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ronny Pfannschmidt --- RELEASING.rst | 6 +++--- downstream/recipes/conda.bash | 7 +++++-- downstream/run_downstream.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index d87ace73..a6f58da9 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -1,10 +1,10 @@ Release Procedure ----------------- -#. Dependening on the magnitude of the changes in the release, consider testing +#. Depending on the magnitude of the changes in the release, consider testing some of the large downstream users of pluggy against the upcoming release. - You can do so with ``uv run downstream/run_downstream.py`` (option - ``--list`` lists recipes). + You can do so with ``uv run downstream/run_downstream.py ``; + use ``--list`` to discover available recipes. #. From a clean work tree, execute:: diff --git a/downstream/recipes/conda.bash b/downstream/recipes/conda.bash index 99a731f4..f785181e 100755 --- a/downstream/recipes/conda.bash +++ b/downstream/recipes/conda.bash @@ -14,8 +14,11 @@ set -eu pip install -e ../.. # Mirror conda's own CI condarc-defaults so tests that create temporary -# environments can resolve packages. -conda config --add channels defaults +# environments can resolve packages without mutating the user's ~/.condarc. +CONDARC="$(mktemp)" +trap 'rm -f "$CONDARC"' EXIT +export CONDARC +conda config --file "$CONDARC" --add channels defaults pytest \ -m 'not integration and not installed' \ diff --git a/downstream/run_downstream.py b/downstream/run_downstream.py index 1bb91b27..da3b3931 100644 --- a/downstream/run_downstream.py +++ b/downstream/run_downstream.py @@ -219,6 +219,18 @@ def run_recipe( profile = recipe.environment if isinstance(profile, EnvironmentScript): + if skip_install or only_install: + flags: list[str] = [] + if skip_install: + flags.append("--skip-install") + if only_install: + flags.append("--only-install") + joined_flags = ", ".join(flags) + print( + f"{joined_flags} is not supported for script-based recipe {name!r}.", + file=sys.stderr, + ) + sys.exit(2) script = DOWNSTREAM_DIR / profile.run run_cmd(["bash", str(script)], cwd=DOWNSTREAM_DIR) return From 2ca24191aa61372ecd5c166164e4152f0fcfc5fe Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 17 May 2026 08:21:21 +0200 Subject: [PATCH 17/17] fix(downstream): revert CONDARC tmpfile change in conda.bash Revert the review suggestion to use a temporary CONDARC file. Exporting CONDARC overrides the devenv's own .condarc, breaking the bootstrap. Go back to the simple conda config --add that worked. Co-authored-by: Cursor AI Co-authored-by: Anthropic Claude Opus 4 --- downstream/recipes/conda.bash | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/downstream/recipes/conda.bash b/downstream/recipes/conda.bash index f785181e..99a731f4 100755 --- a/downstream/recipes/conda.bash +++ b/downstream/recipes/conda.bash @@ -14,11 +14,8 @@ set -eu pip install -e ../.. # Mirror conda's own CI condarc-defaults so tests that create temporary -# environments can resolve packages without mutating the user's ~/.condarc. -CONDARC="$(mktemp)" -trap 'rm -f "$CONDARC"' EXIT -export CONDARC -conda config --file "$CONDARC" --add channels defaults +# environments can resolve packages. +conda config --add channels defaults pytest \ -m 'not integration and not installed' \