Skip to content

[VPEX][6/8] Add local-env uv backend and hidden CLI command#5832

Open
rugpanov wants to merge 1 commit into
dbconnect/05-pipelinefrom
dbconnect/06-command
Open

[VPEX][6/8] Add local-env uv backend and hidden CLI command#5832
rugpanov wants to merge 1 commit into
dbconnect/05-pipelinefrom
dbconnect/06-command

Conversation

@rugpanov

@rugpanov rugpanov commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Why

  • The engine needs a real PackageManager implementation (uv) and a CLI entry point so a user can actually run the feature.
  • Wiring it in while keeping it Hidden lets the command be dogfooded and exercised by acceptance tests without becoming user-visible until the stack is complete.
  • This is the first layer reachable from main, so it also makes the whole libs/localenv package live for the deadcode checker.

What

  • libs/localenv/uv.go — the uv implementation of PackageManager: discover/install uv, install the Python minor, uv sync, seed pip into the venv, validate; plus the pip.confUV_INDEX_URL bridge for Databricks-managed machines.
  • cmd/localenv/ — the command tree matching local-env python sync: a local-env group (Hidden: true), a python subgroup, and the sync verb. Parent nodes use root.ReportUnknownSubcommand; sync uses cobra.NoArgs, resolves flags/bundle target, builds the Pipeline with the uv manager, and renders text or --json. All Cobra Use values + the --json command field come from the libs/localenv constants.
  • cmd/cmd.go — registers the group.

Testing strategy

  • Unit tests for uv helper logic (discovery, pip.conf index-url, arg builders, stderr surfacing) (uv_test.go).
  • Runtime smoke test of the hidden 3-level command: absent from top-level help; help works at each level; unknown subcommand exits non-zero; bare group shows help; flags + mutual-exclusion + NoArgs behave.
  • Gates: go build ./..., go test, golangci-lint, deadcode (whole tree, no pragmas), gofmt — all green.
  • Independently verified by a review subagent (PASS) and by codex across two rounds to a clean pass (added NoArgs; made the unsupported task-level-job-compute path return an actionable error).
  • cmd/localenv/ unit tests are intentionally deferred to the acceptance PR ([VPEX][7/8] Add local-env acceptance tests #5833), per the repo convention that user-visible CLI output is covered by acceptance tests.

About this stack

This is one of a series of small, stacked PRs that together add the databricks local-env python sync command — it provisions a local Python environment (Python version, databricks-connect pin, and dependency constraints) matched to a selected Databricks compute target. The work was split from one large branch into single-concern layers so each is independently reviewable; the command is kept hidden until the final PR so nothing is user-visible mid-stack.

Review bottom-up. Each PR targets the previous one as its base branch, so its diff shows only that layer.

# PR What
1 #5823 foundation: result types + env-key mapping
2 #5824 compute-target resolution
3 #5826 constraint fetch + offline cache
4 #5827 formatting-preserving pyproject.toml merge
5 #5828 six-phase pipeline + detection + package-manager interface
6 #5832 ← you are here uv backend + CLI command (registered hidden)
7 #5833 acceptance tests
8 #5835 unveil (unhide + help + changelog)

This pull request and its description were written by Isaac.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

Based on git history, these people are best suited to review:

  • @pietern -- recent work in cmd/
  • @shreyas-goenka -- recent work in cmd/
  • @simonfaltum -- recent work in cmd/

Eligible reviewers: @andrewnester, @anton-107, @denik, @janniklasrose, @renaudhartert-db

Suggestions based on git history. See OWNERS for ownership rules.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: a7fa50c

Run: 28853000657

Env ❌​FAIL 🟨​KNOWN 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 4 4 233 1055 5:06
❌​ aws windows 3 1 3 4 232 1053 7:26
💚​ aws-ucws linux 4 4 317 973 6:03
💚​ aws-ucws windows 4 4 319 971 6:04
💚​ azure linux 4 4 230 1055 5:35
💚​ azure windows 4 4 232 1053 6:01
💚​ azure-ucws linux 4 4 316 971 6:53
🔄​ azure-ucws windows 2 2 4 318 969 7:33
💚​ gcp linux 4 4 229 1057 5:05
💚​ gcp windows 4 4 231 1055 6:20
11 interesting tests: 4 SKIP, 3 FAIL, 2 flaky, 1 KNOWN, 1 RECOVERED
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
🟨​ TestAccept 💚​R 🟨​K 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions ✅​p ❌​F ✅​p ✅​p 🙈​s 🙈​s 🙈​s 🙈​s 🙈​s 🙈​s
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct ✅​p ❌​F ✅​p ✅​p
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform ✅​p ❌​F ✅​p ✅​p
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🔄​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 🔄​f 💚​R 💚​R
🔄​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 🔄​f 💚​R 💚​R
Top 9 slowest tests (at least 2 minutes):
duration env testname
5:32 gcp windows TestAccept
5:05 azure-ucws windows TestAccept
5:04 aws-ucws windows TestAccept
5:02 azure windows TestAccept
2:58 gcp linux TestAccept
2:58 azure linux TestAccept
2:52 aws linux TestAccept
2:52 aws-ucws linux TestAccept
2:48 azure-ucws linux TestAccept

Sixth in the stacked series. Wires the engine to a runnable CLI command,
registered Hidden so it does not appear in help or completion until the final
unveil PR (it is invocable for dogfooding in the meantime).

- libs/localenv/uv.go: the uv implementation of the PackageManager interface
  (discover/install uv, install Python, uv sync, seed pip, validate the venv),
  plus the pip.conf -> UV_INDEX_URL bridge for Databricks-managed machines.
- cmd/localenv: the command tree matching the target path
  "local-env python sync" — a top group (local-env), a python subgroup, and the
  sync verb. Parent nodes use root.ReportUnknownSubcommand so an unknown
  subcommand errors while a bare group shows help. sync resolves flags/bundle
  target, builds the Pipeline with the uv manager, and renders text or --json.
- cmd/cmd.go: register the group (Hidden:true).

The sync verb rejects stray positional args (cobra.NoArgs) rather than silently
ignoring them, since the target is chosen via flags. Job compute resolution
reads job-level environments/job_clusters; task-level compute is out of scope
and returns an actionable error pointing at --cluster/--serverless.

This is the first layer reachable from main, so it makes the whole
libs/localenv package live for the deadcode checker without any pragmas. Build,
unit tests, lint, and deadcode are clean; the hidden three-level command was
smoke-tested end to end (help at each level, unknown-subcommand errors, flags).

Co-authored-by: Isaac
@rugpanov rugpanov force-pushed the dbconnect/05-pipeline branch from de8581b to be1b6b3 Compare July 7, 2026 08:35
@rugpanov rugpanov force-pushed the dbconnect/06-command branch from ae5563c to a7fa50c Compare July 7, 2026 08:35
@rugpanov rugpanov temporarily deployed to test-trigger-is July 7, 2026 08:35 — with GitHub Actions Inactive
@rugpanov rugpanov temporarily deployed to test-trigger-is July 7, 2026 08:35 — with GitHub Actions Inactive
ZaSkittles pushed a commit to ZaSkittles/cli that referenced this pull request Jul 7, 2026
databricks#5823)

## Why

- The `local-env` feature needs a shared vocabulary before any behavior
can be built: the result shape, the error taxonomy, and how a compute
target maps to an environment key.
- Landing these contract types first lets every later layer (resolve /
fetch / merge / pipeline / command) depend on stable, reviewed
definitions.
- Kept deliberately minimal and dependency-free so it reviews on its own
and stays `unused`/`deadcode`-clean with no consumers yet.

## What

- **`result.go`** — the `--json` / `E_*` output contract: `Result`,
`PipelineError`, `ErrorCode`, `PhaseName`, `PhaseStatus`, `Mode`,
`TargetInfo`, `ResolvedInfo`, `Plan`, `Warning`; plus the command-path
constants (`local-env` / `python` / `sync`) defined in one place.
- **`envkey.go`** — `EnvKeyForServerless` / `EnvKeyForSparkVersion` /
`NormalizeServerless`, and `PythonMinorFromRequires` (clause-aware:
returns the effective highest lower bound of a `requires-python`).
- No wiring into `cmd/`, so the CLI is unchanged. Filesystem/artifact
constants and the phase-order slice deliberately live with their
consumer (PR 5).

## Testing strategy

- Unit tests for the error/type contract (`result_test.go`) and env-key
mapping incl. multi-clause / strict-`>` / no-floor `requires-python`
cases (`envkey_test.go`).
- Gates: `go build`, `go test`, `golangci-lint`, `deadcode`, `gofmt` —
all green.
- Reviewed with codex across several rounds to convergence (all findings
fixed or explicitly rejected as speculative).

---

## About this stack

This is one of a series of small, stacked PRs that together add the
`databricks local-env python sync` command — it provisions a local
Python environment (Python version, `databricks-connect` pin, and
dependency constraints) matched to a selected Databricks compute target.
The work was split from one large branch into single-concern layers so
each is independently reviewable; the command is kept hidden until the
final PR so nothing is user-visible mid-stack.

**Review bottom-up.** Each PR targets the previous one as its base
branch, so its diff shows only that layer.

| # | PR | What |
|---|----|------|
| 1 | **databricks#5823 ← you are here** | foundation: result types + env-key
mapping |
| 2 | databricks#5824 | compute-target resolution |
| 3 | databricks#5826 | constraint fetch + offline cache |
| 4 | databricks#5827 | formatting-preserving pyproject.toml merge |
| 5 | databricks#5828 | six-phase pipeline + detection + package-manager interface
|
| 6 | databricks#5832 | uv backend + CLI command (registered hidden) |
| 7 | databricks#5833 | acceptance tests |
| 8 | databricks#5835 | unveil (unhide + help + changelog) |

This pull request and its description were written by Isaac.
ZaSkittles pushed a commit to ZaSkittles/cli that referenced this pull request Jul 7, 2026
## Why

- `local-env` must turn the user's compute selection into a single
environment key before it can fetch anything, and the selection can come
from several places with a defined precedence.
- Isolating resolution behind a narrow seam keeps it testable without a
live workspace and keeps SDK details out of the engine.

## What

- **`target.go`** — `ResolveTarget` with ordered precedence `--cluster`
→ `--serverless` → `--job` → bundle target, producing a `TargetInfo` +
env key.
- Compute lookups go through the narrow `ComputeClient` interface
(stubbable in tests).
- `ValidateTargetFlags` rejects more than one target flag;
`ResolveTarget` runs it up front so a non-Cobra caller can't silently
resolve the wrong target.
- Classic-compute jobs read the Spark version from the documented first
return of `GetJobSparkVersion` (not the recorded-version third return).

## Testing strategy

- Unit tests against a stub `ComputeClient` covering each precedence
branch, the mutually-exclusive-flags error, and the job classic-compute
contract (`target_test.go`).
- Gates: `go build`, `go test`, `golangci-lint`, `deadcode`, `gofmt` —
all green.
- Reviewed with codex to a clean pass.

---

## About this stack

This is one of a series of small, stacked PRs that together add the
`databricks local-env python sync` command — it provisions a local
Python environment (Python version, `databricks-connect` pin, and
dependency constraints) matched to a selected Databricks compute target.
The work was split from one large branch into single-concern layers so
each is independently reviewable; the command is kept hidden until the
final PR so nothing is user-visible mid-stack.

**Review bottom-up.** Each PR targets the previous one as its base
branch, so its diff shows only that layer.

| # | PR | What |
|---|----|------|
| 1 | databricks#5823 | foundation: result types + env-key mapping |
| 2 | **databricks#5824 ← you are here** | compute-target resolution |
| 3 | databricks#5826 | constraint fetch + offline cache |
| 4 | databricks#5827 | formatting-preserving pyproject.toml merge |
| 5 | databricks#5828 | six-phase pipeline + detection + package-manager interface
|
| 6 | databricks#5832 | uv backend + CLI command (registered hidden) |
| 7 | databricks#5833 | acceptance tests |
| 8 | databricks#5835 | unveil (unhide + help + changelog) |

This pull request and its description were written by Isaac.
ZaSkittles pushed a commit to ZaSkittles/cli that referenced this pull request Jul 7, 2026
…icks#5826)

## Why

- Once a target resolves to an env key, `local-env` needs the pinned
Python version, `databricks-connect` version, and dependency constraints
published for that key.
- The fetch must degrade gracefully offline and distinguish “this
environment isn't published” from “the network is down,” because those
call for different user action.
- The artifact host must be a Databricks-owned, access-controlled
location and must never default to a personal repo — whoever controls
the host controls what the CLI installs.

## What

- **`constraints.go`** — fetches the per-environment `pyproject.toml`,
parses `requires-python`, the `databricks-connect` pin, and `[tool.uv]`
`constraint-dependencies`, and caches it on disk.
- **Host parameterization** — no host is hardcoded:
`RepoConstraintBaseURL` reads the repo (`owner/name`) from the temporary
`DATABRICKS_LOCALENV_CONSTRAINT_REPO` env var and builds a
`raw.githubusercontent.com/<repo>/main` URL; the built-in default is
empty. When unset it returns `""` and `FetchConstraints` reports the
missing source as a fetch-phase `E_FETCH` error (so there is no
untrusted default, and the failure flows through the normal phase/JSON
reporting). Once `databricks/environments` can publish, that becomes the
hardcoded default and the env var is no longer required.
- Failure classification: **404** → `E_ENV_UNSUPPORTED` (no cache
fallback — a distinct non-transient condition); **transport / non-404**
→ `E_FETCH` with fallback to the last-good cached copy.
- Robustness: validate the body (parse + require `requires-python`)
**before** caching so a bad 2xx can't poison the cache; atomic cache
write (mkdir + temp-file + rename); dedicated `http.Client` with a 30s
timeout; body read bounded by `io.LimitReader` at 1 MiB;
`databricks-connect` matched by leading package name under PEP 503
normalization (so `Databricks_Connect` matches, `databricks-connectors`
does not); cache filename = readable slug + sha256 suffix to prevent
collisions.

## Testing strategy

- Unit tests with an `httptest` server: 200-parse, 404 →
`E_ENV_UNSUPPORTED`, transport failure + cache fallback,
missing-`requires-python` rejection, PEP 503 name matching, cache-dir
creation, collision-free filenames, oversized-body rejection
(`constraints_test.go`).
- Host resolution: `TestRepoConstraintBaseURL` (env var → URL, unset →
`""`, whitespace treated as unset) and
`TestFetchConstraintsNoSourceConfigured` (empty host → `E_FETCH` naming
the env var).
- Gates: `go build`, `go test`, `golangci-lint`, `deadcode`, `gofmt` —
all green.
- Reviewed with codex to a clean pass (several fetch/cache edge-case
fixes landed from review).

---

## About this stack

This is one of a series of small, stacked PRs that together add the
`databricks local-env python sync` command — it provisions a local
Python environment (Python version, `databricks-connect` pin, and
dependency constraints) matched to a selected Databricks compute target.
The work was split from one large branch into single-concern layers so
each is independently reviewable; the command is kept hidden until the
final PR so nothing is user-visible mid-stack.

**Review bottom-up.** Each PR targets the previous one as its base
branch, so its diff shows only that layer.

| # | PR | What |
|---|----|------|
| 1 | databricks#5823 | foundation: result types + env-key mapping |
| 2 | databricks#5824 | compute-target resolution |
| 3 | **databricks#5826 ← you are here** | constraint fetch + offline cache |
| 4 | databricks#5827 | formatting-preserving pyproject.toml merge |
| 5 | databricks#5828 | six-phase pipeline + detection + package-manager interface
|
| 6 | databricks#5832 | uv backend + CLI command (registered hidden) |
| 7 | databricks#5833 | acceptance tests |
| 8 | databricks#5835 | unveil (unhide + help + changelog) |

This pull request and its description were written by Isaac.
ZaSkittles pushed a commit to ZaSkittles/cli that referenced this pull request Jul 7, 2026
…atabricks#5827)

## Why

- `local-env` must apply the resolved Python version and constraints to
the user's `pyproject.toml` without disturbing their own content —
comments, ordering, formatting, and unrelated config must survive
untouched.
- Re-running must be safe and idempotent, and a greenfield project needs
a sensible file created from scratch.
- This is the most intricate logic in the feature, so it lands in its
own PR for focused review.

## What

- **`merge.go`** — a formatting-preserving merge that rewrites only the
env-owned regions (`requires-python`, the `databricks-connect` entry in
`[dependency-groups].dev`, and a marker-bracketed managed `[tool.uv]`
block) and preserves every other byte incl. CRLF; idempotent.
`RenderFreshPyproject` builds a complete managed file for a greenfield
project.
- Scoping/robustness: managed `constraint-dependencies` nests
header-less inside an existing user `[tool.uv]` (never a duplicate
header); single- vs multi-line array detection tracks real bracket depth
outside strings/comments; the `databricks-connect` rewrite is confined
to `dev` and leaves trailing comments alone; `requires-python`'s inline
comment is preserved; table-header parsing tolerates inline comments and
recognizes `[[array.of.tables]]`.

## Testing strategy

- Unit tests that parse the merged output as TOML (not just substring
checks), covering idempotency, CRLF preservation, user-key preservation,
the duplicate-`[tool.uv]` case, bracket-in-element arrays,
sibling-group/comment non-clobbering, and `[[tool.uv.index]]` children
(`merge_test.go`).
- Gates: `go build`, `go test`, `golangci-lint`, `deadcode`, `gofmt` —
all green.
- Reviewed with codex to a clean pass (multiple TOML-corruption edge
cases were caught and fixed).

---

## About this stack

This is one of a series of small, stacked PRs that together add the
`databricks local-env python sync` command — it provisions a local
Python environment (Python version, `databricks-connect` pin, and
dependency constraints) matched to a selected Databricks compute target.
The work was split from one large branch into single-concern layers so
each is independently reviewable; the command is kept hidden until the
final PR so nothing is user-visible mid-stack.

**Review bottom-up.** Each PR targets the previous one as its base
branch, so its diff shows only that layer.

| # | PR | What |
|---|----|------|
| 1 | databricks#5823 | foundation: result types + env-key mapping |
| 2 | databricks#5824 | compute-target resolution |
| 3 | databricks#5826 | constraint fetch + offline cache |
| 4 | **databricks#5827 ← you are here** | formatting-preserving pyproject.toml
merge |
| 5 | databricks#5828 | six-phase pipeline + detection + package-manager interface
|
| 6 | databricks#5832 | uv backend + CLI command (registered hidden) |
| 7 | databricks#5833 | acceptance tests |
| 8 | databricks#5835 | unveil (unhide + help + changelog) |

This pull request and its description were written by Isaac.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants