Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/cli/setup-local-provision-conflict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `databricks environments setup-local` now reports a distinct `E_PROVISION_CONFLICT` error code in `--output json` when the project's dependencies conflict with the pins written for the target environment, making the requirements unsatisfiable (the same conflict surfaced as a `W_USER_CONSTRAINT_CONFLICT` warning); it is reported after the project files are written, without attempting the doomed provisioning, while other provisioning failures continue to report `E_PROVISION`. ([#6479](https://github.com/databricks/cli/pull/6479))
2 changes: 2 additions & 0 deletions acceptance/localenv/provision-conflict/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions acceptance/localenv/provision-conflict/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"schemaVersion": 1,
"command": "environments setup-local",
"ok": false,
"mode": "default",
"dryRun": false,
"compute": {
"source": "serverless",
"serverlessVersion": "v4",
"envKey": "serverless/serverless-v4"
},
"resolved": {
"pythonVersion": "3.12",
"dbconnectVersion": "17.2.0",
"artifactSource": "network"
},
"greenfield": false,
"phases": [
{
"phase": "preflight",
"status": "ok"
},
{
"phase": "resolve",
"status": "ok"
},
{
"phase": "fetch",
"status": "ok"
},
{
"phase": "merge",
"status": "ok"
},
{
"phase": "provision",
"status": "error"
},
{
"phase": "validate",
"status": "pending"
}
],
"warnings": [
{
"code": "W_DBCONNECT_PIN_OVERRIDDEN",
"message": "databricks-connect \"databricks-connect~=16.0.0\" is replaced by the environment's \"databricks-connect~=17.2.0\""
},
{
"code": "W_USER_CONSTRAINT_CONFLICT",
"message": "dependency \"pip==24.0\" conflicts with the environment constraint \"pip\u003c24\""
}
],
"error": {
"code": "E_PROVISION_CONFLICT",
"failurePhase": "provision",
"message": "dependency pins conflict with the environment constraints; relax the conflicting pins and re-run (see warnings)",
"diskMutated": true
},
"backupPath": "[TEST_TMP_DIR]/pyproject.toml.bak",
"durationMs": [DURATION_MS]
}
7 changes: 7 additions & 0 deletions acceptance/localenv/provision-conflict/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "demo"
requires-python = ">=3.12"
dependencies = ["pip==24.0"]

[dependency-groups]
dev = ["databricks-connect~=16.0.0"]
6 changes: 6 additions & 0 deletions acceptance/localenv/provision-conflict/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The user pins pip==24.0 while the remote environment constraints pin pip<24, a
# provably disjoint range. The merge phase detects that (W_USER_CONSTRAINT_CONFLICT)
# and setup-local reports E_PROVISION_CONFLICT right away — with the merged pins
# already written to disk (diskMutated=true), the contract the extension's recovery
# flow depends on — instead of spending a doomed Python install and uv sync.
musterr $CLI environments setup-local --serverless-version 4 --output json
22 changes: 22 additions & 0 deletions acceptance/localenv/provision-conflict/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

# setup-local writes pyproject.toml through the merge phase (backing up the
# committed one) before it reports the conflict; the assertion is the JSON output,
# so ignore the mutated file and its backup. No .venv is created because the run
# stops before provisioning.
Ignore = ["pyproject.toml", "pyproject.toml.bak"]

Env.DATABRICKS_LOCALENV_CONSTRAINT_SOURCE_URL_TEST_OVERRIDE = "$DATABRICKS_HOST"

[[Server]]
Pattern = "GET /serverless/serverless-v4/pyproject.toml"
Response.Body = '''
[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]

[tool.uv]
constraint-dependencies = ["pip<24"]
'''
2 changes: 2 additions & 0 deletions cmd/environments/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func errorCodeType(code libslocalenv.ErrorCode) protos.SetupLocalErrorCode {
return protos.SetupLocalErrorCodePythonInstall
case libslocalenv.ErrProvision:
return protos.SetupLocalErrorCodeProvision
case libslocalenv.ErrProvisionConflict:
return protos.SetupLocalErrorCodeProvisionConflict
case libslocalenv.ErrValidate:
return protos.SetupLocalErrorCodeValidate
case libslocalenv.ErrCanceled:
Expand Down
1 change: 1 addition & 0 deletions cmd/environments/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func TestErrorCodeCoversLocalenv(t *testing.T) {
libslocalenv.ErrMerge,
libslocalenv.ErrPythonInstall,
libslocalenv.ErrProvision,
libslocalenv.ErrProvisionConflict,
libslocalenv.ErrValidate,
libslocalenv.ErrCanceled,
}
Expand Down
28 changes: 27 additions & 1 deletion libs/localenv/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,21 @@ func (p *Pipeline) run(ctx context.Context) error {
}
p.markOK(PhaseMerge, "")

// Phase: provision — ensure Python, run uv sync, seed pip.
// The merge proved the written pins conflict with the user's dependencies
// (W_USER_CONSTRAINT_CONFLICT): uv sync would deterministically fail to resolve
// them, so report the conflict now — with a distinct E_PROVISION_CONFLICT code —
// instead of spending a doomed Python install and sync. The constraints are
// already on disk (diskMutated=true), which the extension's recovery flow relies
// on, and the failure is attributed to the provision phase it stands in for.
// Gating on the CLI's own merge detection keeps the code precise (no stderr
// matching, and no false positive on an unrelated sync failure).
p.report(ctx, PhaseProvision)
if p.hasConstraintConflictWarning() {
return p.fail(PhaseProvision, true, NewError(ErrProvisionConflict, nil,
"dependency pins conflict with the environment constraints; relax the conflicting pins and re-run (see warnings)"))
}

// Phase: provision — ensure Python, run uv sync, seed pip.
if err := p.provision(ctx, pyMinor); err != nil {
return err
}
Expand Down Expand Up @@ -529,6 +542,19 @@ func (p *Pipeline) provision(ctx context.Context, pyMinor string) error {
return nil
}

// hasConstraintConflictWarning reports whether the merge phase recorded a provable
// user/environment version conflict (W_USER_CONSTRAINT_CONFLICT). It reads the
// warnings already accumulated on the Result, which the merge phase populates
// before provision runs, so it is only meaningful once merge has completed.
func (p *Pipeline) hasConstraintConflictWarning() bool {
for _, w := range p.res.Warnings {
if w.Code == WarnUserConstraintConflict {
return true
}
}
return false
}

// validate reads the Python and databricks-connect versions from the venv and
// populates the venv path. dbcPin is the resolved databricks-connect pin; when it
// is not managed (opts.SkipDBConnect) the databricks-connect assertion and version
Expand Down
97 changes: 97 additions & 0 deletions libs/localenv/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,103 @@ func TestPipelineProvisionsAndValidatesExisting(t *testing.T) {
assert.FileExists(t, filepath.Join(dir, "pyproject.toml.bak"))
}

func TestPipelineFailsFastOnConstraintConflict(t *testing.T) {
// The user pins pip==24.0 while the environment's constraint-dependencies pin
// pip<24 — a provably disjoint range — so the merge records
// W_USER_CONSTRAINT_CONFLICT. uv sync would deterministically fail to resolve
// that, so the run reports E_PROVISION_CONFLICT right after the merge, at the
// provision phase, with disk already mutated — and never spends a Python install
// or sync (recordingPM records neither call).
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "pyproject.toml"), []byte(`[project]
name = "demo"
requires-python = ">=3.12"
dependencies = ["pip==24.0"]

[dependency-groups]
dev = ["databricks-connect~=16.0.0"]
`), 0o644))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]

[tool.uv]
constraint-dependencies = ["pip<24"]
`))
}))
defer srv.Close()

pm := &recordingPM{fakePM: fakePM{py: "3.12", dbc: "17.2.0"}}
p := &Pipeline{
Mode: ModeDefault, ProjectDir: dir,
ConstraintBaseURL: srv.URL, CacheDir: t.TempDir(),
Flags: ComputeFlags{Serverless: "v4"},
Compute: stubCompute{}, PM: pm,
}
res, err := p.Run(t.Context())
var pe *PipelineError
require.ErrorAs(t, err, &pe)
assert.Equal(t, ErrProvisionConflict, pe.Code)
assert.Equal(t, PhaseProvision, pe.FailurePhase)
assert.True(t, pe.DiskMutated, "the merge wrote the pins before the conflict was reported")
require.NotNil(t, res.Error)
assert.Equal(t, ErrProvisionConflict, res.Error.Code)
// The merge conflict warning that gates the code must be present.
assert.Contains(t, codes(res.Warnings), WarnUserConstraintConflict)
// Fail-fast: neither Python install nor sync was attempted.
assert.Empty(t, pm.minor, "EnsurePython must not run when the conflict is already proven")
assert.Empty(t, pm.provisionPython, "uv sync must not run when the conflict is already proven")
}

func TestPipelineCheckReportsConflictAsWarningNotError(t *testing.T) {
// --dry-run computes a plan and never evaluates provisioning, so the same
// provably-disjoint pins surface only as the W_USER_CONSTRAINT_CONFLICT warning
// with ok=true and no error — the conflict becomes E_PROVISION_CONFLICT only on a
// real run, which is the phase that attempts (and here would fail) provisioning.
// This pins that intended divergence so it cannot regress silently.
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "pyproject.toml"), []byte(`[project]
name = "demo"
requires-python = ">=3.12"
dependencies = ["pip==24.0"]

[dependency-groups]
dev = ["databricks-connect~=16.0.0"]
`), 0o644))
before, _ := os.ReadFile(filepath.Join(dir, "pyproject.toml"))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`[project]
requires-python = ">=3.12"

[dependency-groups]
dev = ["databricks-connect~=17.2.0"]

[tool.uv]
constraint-dependencies = ["pip<24"]
`))
}))
defer srv.Close()

p := &Pipeline{
Mode: ModeDefault, Check: true, ProjectDir: dir,
ConstraintBaseURL: srv.URL, CacheDir: t.TempDir(),
Flags: ComputeFlags{Serverless: "v4"},
Compute: stubCompute{}, PM: fakePM{py: "3.12", dbc: "17.2.0"},
}
res, err := p.Run(t.Context())
require.NoError(t, err)
assert.True(t, res.OK)
assert.Nil(t, res.Error, "a dry run reports the conflict as a warning, not an error")
assert.Contains(t, codes(res.Warnings), WarnUserConstraintConflict)
// A dry run mutates nothing.
after, _ := os.ReadFile(filepath.Join(dir, "pyproject.toml"))
assert.Equal(t, string(before), string(after))
assert.NoFileExists(t, filepath.Join(dir, "pyproject.toml.bak"))
}

func TestPipelineDryRunOmitsFabricatedDBConnectVersion(t *testing.T) {
// A major-only pin like ~=17.0 (serverless, environments#15) is not a concrete
// version. Under --dry-run validate never corrects the reported value, so it
Expand Down
3 changes: 2 additions & 1 deletion libs/localenv/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ const (
ErrWrite ErrorCode = "E_WRITE" // merge: greenfield write failed
ErrMerge ErrorCode = "E_MERGE" // merge: existing-project merge failed
ErrPythonInstall ErrorCode = "E_PYTHON_INSTALL" // provision: uv python install failed
ErrProvision ErrorCode = "E_PROVISION" // provision: uv sync failed
ErrProvision ErrorCode = "E_PROVISION" // provision: uv sync failed (generic)
ErrProvisionConflict ErrorCode = "E_PROVISION_CONFLICT" // provision: merge detected an unsatisfiable version conflict
ErrValidate ErrorCode = "E_VALIDATE" // validate: post-provision version mismatch

// ErrCanceled is not in the spec's error-code table: it reports a user/parent
Expand Down
1 change: 1 addition & 0 deletions libs/telemetry/protos/setup_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
SetupLocalErrorCodeMerge SetupLocalErrorCode = "E_MERGE"
SetupLocalErrorCodePythonInstall SetupLocalErrorCode = "E_PYTHON_INSTALL"
SetupLocalErrorCodeProvision SetupLocalErrorCode = "E_PROVISION"
SetupLocalErrorCodeProvisionConflict SetupLocalErrorCode = "E_PROVISION_CONFLICT"
SetupLocalErrorCodeValidate SetupLocalErrorCode = "E_VALIDATE"
SetupLocalErrorCodeCanceled SetupLocalErrorCode = "E_CANCELED"
)
Expand Down
Loading