Skip to content
Merged
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
2 changes: 1 addition & 1 deletion acceptance/localenv/job-serverless-check/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ EnvMatrix.DATABRICKS_BUNDLE_ENGINE = []
DATABRICKS_LOCALENV_CONSTRAINT_SOURCE = "$DATABRICKS_HOST"

# A serverless job pins its environment_version on the environment spec, so the
# target resolves to that serverless-vN (here v3) rather than defaulting to v4.
# target resolves to that serverless-vN (here v3) rather than defaulting to v5.
[[Server]]
Pattern = "GET /api/2.2/jobs/get"
Response.Body = '''
Expand Down
3 changes: 3 additions & 0 deletions acceptance/localenv/serverless-default-check/out.test.toml

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

13 changes: 13 additions & 0 deletions acceptance/localenv/serverless-default-check/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

>>> [CLI] environments setup-local --job-id 12345 --dry-run
preflight ok check
resolve ok source=job envKey=serverless/serverless-v5
fetch ok source=[DATABRICKS_URL]/serverless/serverless-v5/pyproject.toml fromCache=false
merge ok
provision ok
validate ok
Plan: [TEST_TMP_DIR]/pyproject.toml
changed region: requires-python
changed region: tool.uv.constraint-dependencies
changed region: databricks-connect
Check complete. No files were modified.
1 change: 1 addition & 0 deletions acceptance/localenv/serverless-default-check/script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trace $CLI environments setup-local --job-id 12345 --dry-run
39 changes: 39 additions & 0 deletions acceptance/localenv/serverless-default-check/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = []

[Env]
DATABRICKS_LOCALENV_CONSTRAINT_SOURCE = "$DATABRICKS_HOST"

# A serverless job that pins no environment_version exercises the default-version
# path: ResolveTarget falls back to defaultServerlessVersion (v5), so the target
# resolves to serverless-v5. This is the end-to-end coverage that the v5 default
# actually resolves and fetches, distinct from job-serverless-check which pins v3.
[[Server]]
Pattern = "GET /api/2.2/jobs/get"
Response.Body = '''
{
"job_id": 12345,
"settings": {
"name": "serverless-job-unpinned",
"environments": [
{"environment_key": "default", "spec": {}}
]
}
}
'''

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

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

[tool.uv]
constraint-dependencies = ["pyarrow<19", "pandas<3"]
'''

[[Repls]]
Old = 'uv uv \S+(?: \([^)]+\))?'
New = 'uv [UV_VERSION]'
4 changes: 2 additions & 2 deletions cmd/environments/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (c sdkCompute) GetJobSparkVersion(ctx context.Context, jobID string) (spark
// The serverless environment version (e.g. "4") is recorded on the job's
// environment spec, unlike the bundle path where it is unavailable. Return
// it so ResolveTarget pins the matching serverless-vN instead of defaulting
// to v4. An empty version (older jobs) falls back to v4 in ResolveTarget.
// to v5. An empty version (older jobs) falls back to v5 in ResolveTarget.
version := environmentVersion(job.Settings.Environments[0])
// Tasks can reference any environment_key, so if the job's environments do
// not all share one version there is no single correct local environment
Expand Down Expand Up @@ -146,7 +146,7 @@ func (c sdkCompute) GetJobSparkVersion(ctx context.Context, jobID string) (spark
//
// The version can arrive in either of two fields. environment_version is the
// current one; client is its deprecated predecessor ("Use environment_version
// instead") and is still what some jobs pin. Reading both means the v4 fallback
// instead") and is still what some jobs pin. Reading both means the v5 fallback
// and the divergence guard observe whichever field actually carries the pin,
// rather than treating a client-pinned job as unversioned. base_environment is
// deliberately ignored: it is a path/ID, not a version.
Expand Down
8 changes: 8 additions & 0 deletions libs/localenv/envkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import (
// by 3.10.6, so only the former bumps the minor.
var clauseRe = regexp.MustCompile(`^(>=|<=|===|==|~=|!=|<|>)?\s*(\d+)\.(\d+)(\.\d+)?`)

// defaultServerlessVersion is the serverless environment version used when the
// source (a serverless job with no recorded version, or a bundle that only
// records "serverless") does not pin one. It is a documented stand-in for the
// latest LTS (spec §4.3 / §target-resolution); VS Code resolves the real version
// itself and passes --serverless-version explicitly, so this fallback only
// applies when the version is genuinely unknown.
const defaultServerlessVersion = "v5"

// NormalizeServerless returns the canonical "vN" spelling of a serverless
// version accepting "4", "v4", or "V4".
func NormalizeServerless(version string) string {
Expand Down
14 changes: 7 additions & 7 deletions libs/localenv/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ func ResolveTarget(ctx context.Context, f TargetFlags, c ComputeClient, bt Bundl
}
if isServerless {
// Use the job's recorded serverless environment version when present;
// fall back to v4 when the job did not pin one (documented stand-in from
// the original script, spec §4.3).
// fall back to the default when the job did not pin one (documented
// stand-in, spec §4.3).
v := version
if v == "" {
v = "v4"
v = defaultServerlessVersion
}
return &TargetInfo{
Source: "job",
Expand All @@ -145,12 +145,12 @@ func ResolveTarget(ctx context.Context, f TargetFlags, c ComputeClient, bt Bundl
}

if bt.Serverless {
// Default to serverless-v4: the serverless env version is not recorded
// in the bundle/project (documented stand-in from the original script).
// The bundle records that the target is serverless but not which version,
// so use the default stand-in (spec §4.3).
return &TargetInfo{
Source: "bundle",
ServerlessVersion: "v4",
EnvKey: EnvKeyForServerless("v4"),
ServerlessVersion: NormalizeServerless(defaultServerlessVersion),
EnvKey: EnvKeyForServerless(defaultServerlessVersion),
}, nil
}

Expand Down
15 changes: 10 additions & 5 deletions libs/localenv/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,13 @@ func TestResolveBundleNothingSelected(t *testing.T) {
}

func TestResolveBundleServerless(t *testing.T) {
// The bundle records serverless but no version, so the default stand-in applies.
ti, err := ResolveTarget(t.Context(), TargetFlags{}, stubCompute{}, BundleTarget{Selected: true, Serverless: true})
require.NoError(t, err)
assert.Equal(t, "bundle", ti.Source)
assert.Equal(t, "serverless/serverless-v4", ti.EnvKey)
// Concrete literal, not "serverless-"+defaultServerlessVersion: the default
// is v5, and asserting the constant against itself would pass for any value.
assert.Equal(t, "serverless/serverless-v5", ti.EnvKey)
}

// jobStubCompute returns distinct values for the first (sparkVersion) and third
Expand Down Expand Up @@ -143,13 +146,15 @@ func TestResolveJobServerlessUsesRecordedVersion(t *testing.T) {
assert.Equal(t, "serverless/serverless-v3", ti.EnvKey)
}

func TestResolveJobServerlessEmptyVersionFallsBackToV4(t *testing.T) {
// When the job records no serverless version, ResolveTarget defaults to v4
// (documented stand-in), matching the bundle serverless path.
func TestResolveJobServerlessEmptyVersionFallsBackToDefault(t *testing.T) {
// When the job records no serverless version, ResolveTarget uses the default
// stand-in, matching the bundle serverless path.
c := jobStubCompute{isServerless: true, version: ""}
ti, err := ResolveTarget(t.Context(), TargetFlags{Job: "42"}, c, BundleTarget{})
require.NoError(t, err)
assert.Equal(t, "serverless/serverless-v4", ti.EnvKey)
// Concrete literal, not "serverless-"+defaultServerlessVersion: the default
// is v5, and asserting the constant against itself would pass for any value.
assert.Equal(t, "serverless/serverless-v5", ti.EnvKey)
}

func TestValidateTargetFlagsMutuallyExclusive(t *testing.T) {
Expand Down
Loading