Skip to content

build: harden uv workspace configuration - #1768

Merged
hkad98 merged 3 commits into
masterfrom
jkd/uv-workspace-hardening
Sep 1, 2026
Merged

build: harden uv workspace configuration#1768
hkad98 merged 3 commits into
masterfrom
jkd/uv-workspace-hardening

Conversation

@hkad98

@hkad98 hkad98 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Hardens the uv workspace configuration. Build/tooling config only — no Python source changed.

Two things that were actively broken

members = ["packages/*"] was a booby trap. Any stray directory under packages/ matched the glob and, having no pyproject.toml, broke every uv command in the repo:

error: Workspace member `.../packages/graphify-out` is missing a pyproject.toml (matches: `packages/*`)

Members are now listed explicitly. A new package needs three entries — members, [project].dependencies, and [tool.uv.sources] — and omitting this one fails loudly during resolution rather than silently.

make test-ci was destroying the developer's host venv. The repo is bind-mounted at /data in the test container, so uv run inside resolved the project environment to the host's .venv and rebuilt it against the container's Linux interpreter:

home = /home/tox/.local/share/uv/python/cpython-3.14.0-linux-aarch64-gnu/bin   ← host venv, after a container run

Fixed with ENV UV_PROJECT_ENVIRONMENT=/tmp/uv-project-venv, plus ENV UV_LOCKED=1 so no in-container uv command can rewrite the mounted uv.lock.

Determinism

The image installed the tox group with uv pip install --group tox, which re-resolves fresh from the index and ignores uv.lock. That is why the tox group carried its own uv~=0.12.0 pin, duplicating [tool.uv] required-version. Now it exports from the lock instead, and lock hashes are kept so the install is hash-verified:

uv export --frozen --only-group tox -o /tmp/tox-requirements.txt
uv pip install --system -r /tmp/tox-requirements.txt

The duplicated pin is gone, replaced by constraint-dependencies = ["uv~=0.12.0"]. tox-uv depends on the uv package with no upper bound and the image now takes uv's version from the lock, so a plain uv lock --upgrade could otherwise have put uv 0.13.x in the image and tripped required-version — the same failure the old pin guarded, reached by a different route.

Ergonomics

default-groups = ["dev", "lint", "type", "test", "tox"]. Previously a bare uv sync installed only dev, so make lint / format / type-check failed outright, and make test silently fell through to whatever tox was on PATH — a system tox without the tox-uv plugin dies on runner 'uv-venv-lock-runner' is not available.

TOX ?= uv run tox in project_common.mk, overridden to bare tox in the image, so container runs use the lock-pinned system tox already installed there rather than first syncing the whole workspace into a throwaway env to obtain the same tox (measured: 58 packages, ~7s per container).

The root test group gains jsonschema and drops the version bounds its members already declare. Without jsonschema, gooddata-flexconnect's tests could not even be collected from the root venv — the environment scripts/validate_python.sh uses for --test-filter runs. Before/after in that package: ModuleNotFoundError → 61 passed.

The staging targets use uv run --locked python instead of bare python; staging-tests.yaml syncs .venv but never activates it, so those two scripts ran without orjson, pyyaml or requests.

Cleanup

  • Dead wheel_build_env removed from the 8 package tox.ini files. tox reported it as an unused key — uv-venv-lock-runner installs from the lock and never builds a package env, and no .pkg env is created in practice.
  • gooddata-pipelines' test group loses its Poetry-style specifier syntax while keeping its major-version ceilings.
  • pre-commit's ruff pinned to v0.15.20, matching the lint group that already resolved to 0.15.20 while the hook ran 0.15.1.

Accepted cost

default-groups widens the three CI steps that use uv sync --group ... from two groups to five — 9 packages, ~86 MB per job (rw-python-tests.yaml:59,73, staging-tests.yaml:61). The --only-group steps are unaffected. Narrowing them with --no-default-groups is a pessimization on its own: the make target that follows each step calls uv run, which re-widens the environment, so you get two install passes instead of one. Making it genuinely narrower requires switching TY = uv run ty and make test-docs-scripts to direct .venv/bin/... invocations — a separate decision about the tool-invocation convention.

uv.lock resolved versions are unchanged except for jsonschema and its three transitive deps; every other version = line is byte-identical to master.

Smaller image

tox-uv is a thin wrapper: the plugin lives in tox-uv-bare, and the only thing the wrapper adds is a dependency on the uv PyPI package — a 58MB binary the image already has (COPYed from ghcr.io/astral-sh/uv) and the host already has on PATH. Installing it again bought nothing except an overwrite of the COPYed binary.

Test image: 378MB → 302MB, 13 packages installed instead of 15. tox --version still reports the plugin registered, since tox-uv-bare was always what provided it.

Two knock-on effects: constraint-dependencies now has nothing to constrain (uv is gone from the lock entirely) and is removed; and the COPY is now the image's only source of uv rather than something the lock overwrote, so the base image is pinned to exact 0.12.5 instead of the floating 0.12 tag.

Lint is now actually enforced

The job named lint-and-format-check only ever ran make format. Ruff's linter was never enforced in CI — only by pre-commit, which runs on changed files. Three violations reached master as a result, and make lint failed on a clean checkout:

SIM103   packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py:92   Return the condition directly
PLC0415  packages/gooddata-eval/tests/test_agentic_runner.py:84                import should be at top-level
PLC0415  packages/gooddata-eval/tests/test_agentic_runner.py:197               import should be at top-level

Fixed, then the missing make lint step added so the job does what its name says.

Both PLC0415 sites imported symbols already imported at module topAgenticEvalOutcome identically, AGENTIC_TEST_KINDS from a module the file already imports from — so they were redundant, not deliberate deferrals for a cycle or an expensive import. SIM103 collapsed an if isinstance(...): return True / return False pair into returning the isinstance directly, keeping the comment about the 1–4% flake rate. No behaviour change: the chain returns the same value for every input, and the moved imports resolve to the same objects.

This pairs with default-groups above: a bare uv sync now installs ruff, so make lint works locally without --all-groups.

Also

gooddata-eval was the only package with no .gitignore, so make test there left .coverage, coverage.xml, .tox/ and the json report commit-ready. Added one mirroring its siblings.

Follow-ups, deliberately not in this PR

  • make docs is broken for all 4 packages that define it. gooddata-flexconnect and gooddata-flight-server have no docs/ directory at all; gooddata-fdw and gooddata-pandas have [testenv:docs] blocks whose deps and skip_install are dead keys under uv-venv-lock-runner (tox config -e docs# !!! unused: deps, skip_install), so sphinx-build is never installed — and their docs/requirements.txt references unresolvable -e ../gooddata-sdk paths and pins Sphinx~=5.1.1 under basepython = python3.14. Left entirely alone here rather than half-cleaned.
  • gooddata-api-client packaging — separate plan; it still has no pyproject.toml and publishes Requires-Python: >=3.6.
  • Three CI uv sync --group ... steps could be narrowed, but not with --no-default-groups alone — see Accepted cost above. Doing it properly means switching TY = uv run ty and make test-docs-scripts to direct .venv/bin/... invocations, which is a decision about the tool-invocation convention rather than a tweak.

Investigated and deliberately not changed

.tox looked like it had the same bind-mount problem UV_PROJECT_ENVIRONMENT fixes for .venv — container runs leave Linux venvs in packages/*/.tox/. It doesn't. Tested directly: a .tox/py314 created by a container (home = /usr/local/bin, Linux 3.14.3) then used by a host run → tox detected the foreign interpreter, transparently recreated the env (home = /opt/homebrew/opt/python@3.14/bin, 3.14.6), 8 passed; a second run's setup dropped 1.35s → 0.77s, so reuse still works. Categorically unlike the .venv bug, where uv left the host venv pointing at a dead interpreter with no recovery. TOX_WORK_DIR would have cost .tox reuse to fix nothing.

Verification

Check Result
uv lock --check clean
make format 569 files, clean
make lint All checks passed — first time on a clean checkout
make type-check 8/8 packages pass
make test-docs-scripts 92 passed
make test — gooddata-sdk, py314, recreated 553 passed, 2 skipped, 3 xfailed
make test — gooddata-eval, py314, recreated 473 passed
make test — gooddata-pipelines, py314, recreated 195 passed
make test-ci — gooddata-dbt, py314 and py310 containers 8 passed each
Test image size 378MB → 302MB
In-image versions uv 0.12.5, tox-uv-bare 1.35.2 — exactly as locked
Host .venv after container runs intact
pre-commit run --files <changed> all hooks pass
uv run pytest from packages/gooddata-flexconnect ModuleNotFoundError → 61 passed
CI job simulated (rm -rf .venv && uv sync --only-group lint --lockedmake formatmake lint) 1 package (ruff 0.15.20), both clean

py310 was built and run specifically because the exported tox requirements carry python_full_version < '3.11' markers (tomli, typing-extensions): the py310 image installs a different set than py314.

uv.lock resolved versions are unchanged from master except: jsonschema + 3 transitive deps added, and tox-uv + uv removed.

Summary by CodeRabbit

  • Improvements
    • Development and test workflows now use locked, reproducible dependency versions.
    • Test commands run consistently across local and containerized environments.
    • Dependency configuration is more consistent across project packages.
    • Updated linting tooling provides the latest supported checks.
    • Improved reliability when handling remote connection interruptions.
  • Chores
    • Simplified test-environment configuration and cleaned up temporary installation files.
    • Staging data commands now use the managed project environment.
    • Added clearer workspace and test dependency configuration for reliable project setup.
    • Added standard exclusions for evaluation-package test artifacts.

Fixes several ways the uv setup was already broken or could break silently.

Workspace members are now listed explicitly instead of matching `packages/*`.
Any stray directory under packages/ -- tool output, scratch, build leftovers --
matched that glob and, having no pyproject.toml, broke every uv command in the
repo with "missing a pyproject.toml".

The Docker image installs the tox group from uv.lock via `uv export` instead of
`uv pip install --group`, which re-resolves fresh from the index and ignores the
lock. That re-resolution is why the tox group carried its own `uv~=0.12.0` pin,
duplicating [tool.uv] required-version; the pin is removed, so the export and
the removal must stay in the same commit. Lock hashes are no longer stripped,
so the install is hash-verified at no measurable cost.

`constraint-dependencies` now bounds the locked uv. tox-uv depends on the uv
PyPI package with no upper bound, and the image takes uv's version straight from
the lock, so a plain `uv lock --upgrade` could have put uv 0.13.x in the image
and tripped required-version at runtime -- the same failure the removed pin
guarded, reached by a different route.

UV_PROJECT_ENVIRONMENT redirects the in-container project environment. The repo
is bind-mounted at /data, so `uv run` inside the test container was rebuilding
the developer's host .venv against the container's Linux interpreter, leaving
the host with a venv pointing at a non-existent python. UV_LOCKED additionally
stops any in-container uv command rewriting the mounted uv.lock.

`TOX ?= uv run tox` in project_common.mk, overridden to bare `tox` in the image,
so container runs use the lock-pinned system tox already installed there instead
of first syncing the whole workspace into a throwaway env to obtain the same tox
(measured: 58 packages, ~7s per container).

default-groups covers dev/lint/type/test/tox so a bare `uv sync` produces a venv
that can run every make target. Previously only `dev` was installed, so
lint/format/type-check failed outright and `make test` silently fell through to
whatever tox was on PATH -- a system tox without the tox-uv plugin then dies on
"runner 'uv-venv-lock-runner' is not available". Accepted cost: the three CI
steps that use `uv sync --group ...` widen from two groups to five, about 9
packages and 86MB per job; the `--only-group` steps are unaffected.

The root test group gains jsonschema and drops the version bounds that each
member's own test group already declares. Without jsonschema, gooddata-flexconnect's
tests could not even be collected from the root venv, which is the environment
scripts/validate_python.sh uses for filtered runs. gooddata-pipelines' test group
loses its Poetry-style specifier syntax while keeping its major-version ceilings.

The staging make targets use `uv run --locked python` rather than bare `python`;
the staging workflow syncs .venv but never activates it, so those two scripts ran
without orjson, pyyaml or requests.

Dead `wheel_build_env` is removed from the eight package tox.ini files. tox
itself reported it as an unused key: uv-venv-lock-runner installs from the lock
and never builds a package env, and no .pkg env is created in practice.

pre-commit's ruff is pinned to v0.15.20 to match the lint group, which was
already resolving to 0.15.20 while the hook ran 0.15.1.

jira: trivial
risk: low
@hkad98
hkad98 requested review from lupko and pcerny as code owners September 1, 2026 12:03
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 7cd0df53-6539-44a8-b6e9-499cfa85849f

📥 Commits

Reviewing files that changed from the base of the PR and between a1b29d9 and b839f93.

📒 Files selected for processing (3)
  • .github/workflows/rw-python-tests.yaml
  • packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py
  • packages/gooddata-eval/tests/test_agentic_runner.py

Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.


📝 Walkthrough

Walkthrough

The pull request configures an explicit uv workspace, aligns test dependencies, and updates tox execution for locked environments. Docker, Make targets, and package tox files use the new execution model. The Ruff pre-commit hook and lint workflow are updated.

Changes

Workspace and test tooling

Layer / File(s) Summary
Workspace and dependency contract
pyproject.toml, packages/gooddata-pipelines/pyproject.toml
The root project now defines explicit uv workspace settings and members. Root test dependencies mirror workspace package dependencies. The pipelines package uses direct PEP 508 constraints and updates its pytest requirement.
Locked tox execution
Dockerfile, project_common.mk, Makefile, packages/*/tox.ini
Docker exports tox dependencies from the frozen lockfile and installs them system-wide. Make targets use locked uv execution. Common test targets accept a configurable tox command. Package tox files remove wheel_build_env.
Tooling and validation
.pre-commit-config.yaml, .github/workflows/rw-python-tests.yaml, packages/gooddata-eval/.gitignore, packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py, packages/gooddata-eval/tests/test_agentic_runner.py
Ruff hooks use revision v0.15.20. The workflow runs make lint. The evaluation package ignores generated artifacts. Retry classification and test imports are simplified.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to b839f

This build and tooling configuration change has no actionable merge-blocking risk remaining and is merge-ready after normal checks and review.

Suggested reviewers: lupko, pcerny, tomkess

Poem

A rabbit checks the workspace tree
Frozen locks keep tests in glee
Tox hops through its chosen route
Ruff shines bright without a doubt
Clean staging follows suit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: hardening the uv workspace and build configuration.
Full details: Docstring Coverage

Explanation

Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.82%. Comparing base (f25f9fd) to head (b839f93).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1768      +/-   ##
==========================================
- Coverage   80.83%   80.82%   -0.01%     
==========================================
  Files         272      272              
  Lines       19416    19414       -2     
==========================================
- Hits        15694    15692       -2     
  Misses       3722     3722              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tox-uv is a thin wrapper: the plugin itself lives in tox-uv-bare, and the only
thing the wrapper adds is a dependency on the `uv` PyPI package. That package is
a 58MB binary the test image already has (COPYed from ghcr.io/astral-sh/uv) and
the host already has on PATH, so installing it again bought nothing except an
overwrite of the COPYed binary.

Dropping it removes two distributions from the lock and shrinks each test image
from 378MB to 302MB. `tox --version` still reports the plugin registered, since
tox-uv-bare was always the thing providing it.

Because the wrapper's `uv` dependency is gone, [tool.uv] constraint-dependencies
has nothing left to constrain and is removed. That also inverts the earlier
argument for leaving the base image tag floating: the COPY is now the image's
only source of uv rather than something the lock overwrote, so it is pinned to
an exact 0.12.5 instead of the 0.12 tag.

Also adds a .gitignore to gooddata-eval, the only package without one. Its
sibling packages all ignore .tox/, .coverage, coverage.xml and the json report;
without them, `make test` in that package leaves the artifacts staged for an
accidental commit.

jira: trivial
risk: low
The job named `lint-and-format-check` only ever ran `make format`, so ruff's
linter was never enforced anywhere except pre-commit -- which runs on changed
files only. Three violations reached master as a result, and `make lint` has
been failing on a clean checkout.

Fixes all three, then adds the missing step so the job does what its name says:

- sse_client.py `_is_retryable_exc` ended with an `if isinstance(...): return
  True` / `return False` pair (SIM103). Collapsed to returning the isinstance
  directly; the comment explaining why RemoteProtocolError is retryable moves
  above the return and is unchanged.
- test_agentic_runner.py imported `AGENTIC_TEST_KINDS` and `AgenticEvalOutcome`
  inside two test bodies (PLC0415, twice). Both names were already imported at
  module top -- `AgenticEvalOutcome` identically, `AGENTIC_TEST_KINDS` from a
  module the file already imports from -- so the local imports were redundant,
  not deliberate deferrals. Removed, and AGENTIC_TEST_KINDS added to the
  existing top-level import.

No behaviour change: the isinstance chain returns the same value for every
input, and the moved imports resolve to the same objects.

jira: trivial
risk: low
@hkad98
hkad98 merged commit 12861e2 into master Sep 1, 2026
14 checks passed
@hkad98
hkad98 deleted the jkd/uv-workspace-hardening branch September 1, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants