From 02782c9e7c74afafc064d4adb318900b42cd13f6 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 10:17:50 +0200 Subject: [PATCH 01/14] logdiag: fill Locations from Paths Co-authored-by: Isaac --- bundle/bundle.go | 12 +++++++++ libs/logdiag/logdiag.go | 21 ++++++++++++++++ libs/logdiag/logdiag_test.go | 47 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/bundle/bundle.go b/bundle/bundle.go index 68654bead7c..823c42a618f 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -22,10 +22,12 @@ import ( "github.com/databricks/cli/bundle/statemgmt/resourcestate" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cache" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/fileset" "github.com/databricks/cli/libs/locker" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structpath" libsync "github.com/databricks/cli/libs/sync" "github.com/databricks/cli/libs/tags" "github.com/databricks/cli/libs/telemetry/protos" @@ -236,6 +238,14 @@ func Load(ctx context.Context, path string) (*Bundle, error) { return b, nil } +// setLocationsOf lets logdiag fill diagnostic locations from the paths into the bundle configuration. +// The closure reads b.Config at log time, so later configuration changes are seen. +func setLocationsOf(ctx context.Context, b *Bundle) { + logdiag.SetLocationsOf(ctx, func(p *structpath.PathNode) []diag.Location { + return b.Config.GetLocationsOf(p) + }) +} + // MustLoad returns a bundle configuration. // The errors are recorded by logdiag, check with logdiag.HasError(). func MustLoad(ctx context.Context) *Bundle { @@ -254,6 +264,7 @@ func MustLoad(ctx context.Context) *Bundle { logdiag.LogError(ctx, err) return nil } + setLocationsOf(ctx, b) return b } @@ -281,6 +292,7 @@ func TryLoad(ctx context.Context) *Bundle { logdiag.LogError(ctx, err) return nil } + setLocationsOf(ctx, b) return b } diff --git a/libs/logdiag/logdiag.go b/libs/logdiag/logdiag.go index 28ed3b5ba21..d74436f3455 100644 --- a/libs/logdiag/logdiag.go +++ b/libs/logdiag/logdiag.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/structs/structpath" ) type keyType int @@ -27,6 +28,10 @@ type LogDiagData struct { // Root to resolve location against Root string + // LocationsOf resolves the locations of a configuration path. + // It is used to fill Locations of diagnostics that only set Paths. Use SetLocationsOf() to set. + LocationsOf func(path *structpath.PathNode) []diag.Location + // If Collect is true, diagnostics are appended to Collected. Use SetCollected() to set. Collect bool Collected []diag.Diagnostic @@ -108,6 +113,14 @@ func SetRoot(ctx context.Context, root string) { read(ctx).Root = root } +func SetLocationsOf(ctx context.Context, fn func(path *structpath.PathNode) []diag.Location) { + val := read(ctx) + val.mu.Lock() + defer val.mu.Unlock() + + val.LocationsOf = fn +} + func SetCollect(ctx context.Context, collect bool) { val := read(ctx) val.mu.Lock() @@ -157,6 +170,14 @@ func LogDiag(ctx context.Context, d diag.Diagnostic) { return } + if len(d.Locations) == 0 && val.LocationsOf != nil { + var locations []diag.Location + for _, p := range d.Paths { + locations = append(locations, val.LocationsOf(p)...) + } + d.Locations = locations + } + for i := range d.Locations { // Make location relative to the root if d.Locations[i].File != "" { diff --git a/libs/logdiag/logdiag_test.go b/libs/logdiag/logdiag_test.go index ecf12ee78d1..21a036ab257 100644 --- a/libs/logdiag/logdiag_test.go +++ b/libs/logdiag/logdiag_test.go @@ -4,7 +4,9 @@ import ( "errors" "testing" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structpath" "github.com/stretchr/testify/assert" ) @@ -22,3 +24,48 @@ func TestIsolatedContext(t *testing.T) { assert.False(t, logdiag.HasError(ctx)) assert.Empty(t, logdiag.FlushCollected(ctx)) } + +func TestLocationsFilledFromPaths(t *testing.T) { + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + + var calls []string + logdiag.SetLocationsOf(ctx, func(p *structpath.PathNode) []diag.Location { + calls = append(calls, p.String()) + return []diag.Location{{File: p.String(), Line: 1}} + }) + + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "paths only", + Paths: structpath.MustParsePaths("a.b", "c"), + }) + explicit := []diag.Location{{File: "explicit", Line: 7}} + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "explicit", + Paths: structpath.MustParsePaths("a.b"), + Locations: explicit, + }) + + got := logdiag.FlushCollected(ctx) + assert.Len(t, got, 2) + assert.Equal(t, []string{"a.b", "c"}, calls) + assert.Equal(t, []diag.Location{{File: "a.b", Line: 1}, {File: "c", Line: 1}}, got[0].Locations) + assert.Equal(t, explicit, got[1].Locations) +} + +func TestLocationsNilResolver(t *testing.T) { + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "no resolver", + Paths: structpath.MustParsePaths("a.b"), + }) + + got := logdiag.FlushCollected(ctx) + assert.Len(t, got, 1) + assert.Empty(t, got[0].Locations) +} From 1f5d3c33043e6cad76103afcb5686aaa584b7923 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 10:21:28 +0200 Subject: [PATCH 02/14] Drop Locations that duplicate Paths Co-authored-by: Isaac --- bundle/apps/validate.go | 3 +-- bundle/bundle.go | 2 +- .../apply_source_linked_deployment_preset.go | 9 ++++----- .../apply_source_linked_deployment_preset_test.go | 4 ++-- bundle/config/mutator/reject_internal_resources.go | 7 +++---- bundle/config/mutator/resourcemutator/run_as.go | 7 +++---- bundle/config/validate/files_to_sync.go | 3 +-- bundle/config/validate/job_task_cluster_spec.go | 9 ++++----- .../validate/no_interpolation_in_bundle_name.go | 9 ++++----- bundle/config/validate/required.go | 7 +++---- bundle/config/validate/validate_artifact_path.go | 7 +++---- .../config/validate/validate_artifact_path_test.go | 14 ++++++-------- bundle/config/validate/validate_dashboard_etags.go | 7 +++---- .../config/validate/validate_deployment_fields.go | 7 +++---- .../config/validate/validate_genie_space_etags.go | 7 +++---- .../validate/validate_job_run_idempotency_token.go | 7 +++---- bundle/config/validate/validate_sync_patterns.go | 7 +++---- bundle/config/validate/validate_volume_path.go | 9 ++++----- .../tests/sync_include_exclude_no_matches_test.go | 10 ++++++---- 19 files changed, 60 insertions(+), 75 deletions(-) diff --git a/bundle/apps/validate.go b/bundle/apps/validate.go index 568b4db02fc..cd88d0dc39b 100644 --- a/bundle/apps/validate.go +++ b/bundle/apps/validate.go @@ -159,8 +159,7 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc ref.permission, appKey, ), - Paths: []*structpath.PathNode{appPath}, - Locations: b.Config.NearestLocations(appPath), + Paths: []*structpath.PathNode{appPath}, }) } diff --git a/bundle/bundle.go b/bundle/bundle.go index 823c42a618f..8005d7cd668 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -242,7 +242,7 @@ func Load(ctx context.Context, path string) (*Bundle, error) { // The closure reads b.Config at log time, so later configuration changes are seen. func setLocationsOf(ctx context.Context, b *Bundle) { logdiag.SetLocationsOf(ctx, func(p *structpath.PathNode) []diag.Location { - return b.Config.GetLocationsOf(p) + return b.Config.NearestLocations(p) }) } diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset.go b/bundle/config/mutator/apply_source_linked_deployment_preset.go index 698696b6e5d..96239abcf53 100644 --- a/bundle/config/mutator/apply_source_linked_deployment_preset.go +++ b/bundle/config/mutator/apply_source_linked_deployment_preset.go @@ -73,11 +73,10 @@ func (m *applySourceLinkedDeploymentPreset) Apply(ctx context.Context, b *bundle path := structpath.NewPath(nil, "workspace", "file_path") diags = diags.Append( diag.Diagnostic{ - Severity: diag.Warning, - Summary: "workspace.file_path setting will be ignored in source-linked deployment mode", - Detail: "In source-linked deployment files are not copied to the destination and resources use source files instead", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.NearestLocations(path), + Severity: diag.Warning, + Summary: "workspace.file_path setting will be ignored in source-linked deployment mode", + Detail: "In source-linked deployment files are not copied to the destination and resources use source files instead", + Paths: []*structpath.PathNode{path}, }, ) } diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset_test.go b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go index 8d50788a3b3..ece102fb358 100644 --- a/bundle/config/mutator/apply_source_linked_deployment_preset_test.go +++ b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go @@ -138,12 +138,12 @@ func TestApplyPresetsSourceLinkedDeployment(t *testing.T) { if tt.expectedWarning != "" { require.Equal(t, tt.expectedWarning, diags[0].Summary) - require.NotEmpty(t, diags[0].Locations) + require.NotEmpty(t, diags[0].Paths) } if tt.expectedError != "" { require.Equal(t, tt.expectedError, diags[0].Summary) - require.NotEmpty(t, diags[0].Locations) + require.NotEmpty(t, diags[0].Paths) } require.Equal(t, tt.expectedValue, b.Config.Presets.SourceLinkedDeployment) diff --git a/bundle/config/mutator/reject_internal_resources.go b/bundle/config/mutator/reject_internal_resources.go index de44c09298c..071e7065acd 100644 --- a/bundle/config/mutator/reject_internal_resources.go +++ b/bundle/config/mutator/reject_internal_resources.go @@ -24,10 +24,9 @@ func (m *rejectInternalResources) Apply(ctx context.Context, b *bundle.Bundle) d var diags diag.Diagnostics if b.Config.Resources.HasInternalResources() { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "Internal resources cannot be set in bundle configuration", - Paths: structpath.NewPathSlice("resources"), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources")), + Severity: diag.Error, + Summary: "Internal resources cannot be set in bundle configuration", + Paths: structpath.NewPathSlice("resources"), }) } diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index 9a3f4b4544c..df852ee75b7 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -236,10 +236,9 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { setRunAsForJobs(b) return diag.Diagnostics{ { - Severity: diag.Warning, - Summary: "You are using the legacy mode of run_as. The support for this mode is experimental and might be removed in a future release of the CLI. In order to run the pipelines in your DABs project as the run_as user this mode changes the owners of the pipelines to the run_as identity, which requires the user deploying the bundle to be a workspace admin, and also a Metastore admin if the pipeline target is in UC.", - Paths: structpath.NewPathSlice("experimental", "use_legacy_run_as"), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "experimental", "use_legacy_run_as")), + Severity: diag.Warning, + Summary: "You are using the legacy mode of run_as. The support for this mode is experimental and might be removed in a future release of the CLI. In order to run the pipelines in your DABs project as the run_as user this mode changes the owners of the pipelines to the run_as identity, which requires the user deploying the bundle to be a workspace admin, and also a Metastore admin if the pipeline target is in UC.", + Paths: structpath.NewPathSlice("experimental", "use_legacy_run_as"), }, } } diff --git a/bundle/config/validate/files_to_sync.go b/bundle/config/validate/files_to_sync.go index b719d46282c..22216f3a785 100644 --- a/bundle/config/validate/files_to_sync.go +++ b/bundle/config/validate/files_to_sync.go @@ -62,8 +62,7 @@ func (v *filesToSync) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnost Summary: "There are no files to sync, please check your .gitignore and sync.exclude configuration", // Show all locations where sync.exclude is defined, since merging // sync.exclude is additive. - Locations: b.Config.NearestLocations(path), - Paths: []*structpath.PathNode{path}, + Paths: []*structpath.PathNode{path}, }) } diff --git a/bundle/config/validate/job_task_cluster_spec.go b/bundle/config/validate/job_task_cluster_spec.go index b4ed9fecb53..fbb658374eb 100644 --- a/bundle/config/validate/job_task_cluster_spec.go +++ b/bundle/config/validate/job_task_cluster_spec.go @@ -89,11 +89,10 @@ func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.Path ) diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "Missing required cluster or environment settings", - Detail: detail, - Locations: b.Config.NearestLocations(taskPath), - Paths: []*structpath.PathNode{taskPath}, + Severity: diag.Error, + Summary: "Missing required cluster or environment settings", + Detail: detail, + Paths: []*structpath.PathNode{taskPath}, }) } } diff --git a/bundle/config/validate/no_interpolation_in_bundle_name.go b/bundle/config/validate/no_interpolation_in_bundle_name.go index c4e2aa3f8f9..475906ec628 100644 --- a/bundle/config/validate/no_interpolation_in_bundle_name.go +++ b/bundle/config/validate/no_interpolation_in_bundle_name.go @@ -25,11 +25,10 @@ func (m *noInterpolationInBundleName) Apply(ctx context.Context, b *bundle.Bundl logdiag.LogDiag(ctx, diag.Diagnostic{ Severity: diag.Warning, Summary: `Please do not use variable interpolation in the name of your bundle. The name of your bundle -is a part of the path at which your bundle state is stored at by default. Parameterizing it at -runtime can have unexpected consequences like duplicate deployments or resources not being -cleaned up during bundle destroy.`, - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "bundle", "name")), - Paths: structpath.NewPathSlice("bundle", "name"), + is a part of the path at which your bundle state is stored at by default. Parameterizing it at + runtime can have unexpected consequences like duplicate deployments or resources not being + cleaned up during bundle destroy.`, + Paths: structpath.NewPathSlice("bundle", "name"), }) } diff --git a/bundle/config/validate/required.go b/bundle/config/validate/required.go index d39eee5bce0..8f94d4b17c8 100644 --- a/bundle/config/validate/required.go +++ b/bundle/config/validate/required.go @@ -129,10 +129,9 @@ func errorForMissingFields(ctx context.Context, b *bundle.Bundle) diag.Diagnosti if strings.TrimSpace(warehouse.Name) == "" { path := structpath.NewPath(nil, "resources", "sql_warehouses", key) diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "sql_warehouse name is required", - Locations: b.Config.NearestLocations(path), - Paths: []*structpath.PathNode{path}, + Severity: diag.Error, + Summary: "sql_warehouse name is required", + Paths: []*structpath.PathNode{path}, }) } } diff --git a/bundle/config/validate/validate_artifact_path.go b/bundle/config/validate/validate_artifact_path.go index 6d6a83e86e9..8d3e5328855 100644 --- a/bundle/config/validate/validate_artifact_path.go +++ b/bundle/config/validate/validate_artifact_path.go @@ -83,10 +83,9 @@ func (v *validateArtifactPath) Apply(ctx context.Context, b *bundle.Bundle) diag wrapErrorMsg := func(s string) diag.Diagnostics { return diag.Diagnostics{ { - Summary: s, - Severity: diag.Error, - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "workspace", "artifact_path")), - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Summary: s, + Severity: diag.Error, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }, } } diff --git a/bundle/config/validate/validate_artifact_path_test.go b/bundle/config/validate/validate_artifact_path_test.go index 778326430e0..d4e30b2e708 100644 --- a/bundle/config/validate/validate_artifact_path_test.go +++ b/bundle/config/validate/validate_artifact_path_test.go @@ -80,10 +80,9 @@ func TestValidateArtifactPath(t *testing.T) { assertDiags := func(t *testing.T, diags diag.Diagnostics, expected string) { assert.Len(t, diags, 1) assert.Equal(t, diag.Diagnostics{{ - Severity: diag.Error, - Summary: expected, - Locations: []diag.Location{{File: "file", Line: 1, Column: 1}}, - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Severity: diag.Error, + Summary: expected, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }}, diags) } @@ -167,10 +166,9 @@ func TestValidateArtifactPathWithInvalidPaths(t *testing.T) { diags := ValidateArtifactPath().Apply(t.Context(), b) require.Equal(t, diag.Diagnostics{{ - Severity: diag.Error, - Summary: "expected UC volume path to be in the format /Volumes////..., got " + p, - Locations: []diag.Location{{File: "config.yml", Line: 1, Column: 2}}, - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Severity: diag.Error, + Summary: "expected UC volume path to be in the format /Volumes////..., got " + p, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }}, diags) } } diff --git a/bundle/config/validate/validate_dashboard_etags.go b/bundle/config/validate/validate_dashboard_etags.go index 265683c9f65..853fecfe5fb 100644 --- a/bundle/config/validate/validate_dashboard_etags.go +++ b/bundle/config/validate/validate_dashboard_etags.go @@ -25,10 +25,9 @@ func (v *validateDashboardEtags) Apply(ctx context.Context, b *bundle.Bundle) di if dashboard.Etag != "" { return diag.Diagnostics{ { - Severity: diag.Error, - Summary: fmt.Sprintf("dashboard %q has an etag set. Etags must not be set in bundle configuration", dashboard.DisplayName), - Paths: structpath.NewPathSlice("resources", "dashboards", k), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "dashboards", k)), + Severity: diag.Error, + Summary: fmt.Sprintf("dashboard %q has an etag set. Etags must not be set in bundle configuration", dashboard.DisplayName), + Paths: structpath.NewPathSlice("resources", "dashboards", k), }, } } diff --git a/bundle/config/validate/validate_deployment_fields.go b/bundle/config/validate/validate_deployment_fields.go index 4d2fbd6fb0a..605eebc2ccf 100644 --- a/bundle/config/validate/validate_deployment_fields.go +++ b/bundle/config/validate/validate_deployment_fields.go @@ -32,10 +32,9 @@ func (v *validateDeploymentFields) Apply(_ context.Context, b *bundle.Bundle) di } path := structpath.NewPath(nil, "resources", resourceType, name, "deployment", field) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: field + " must not be set in bundle configuration; it is managed by Declarative Automation Bundles", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.NearestLocations(path), + Severity: diag.Error, + Summary: field + " must not be set in bundle configuration; it is managed by Declarative Automation Bundles", + Paths: []*structpath.PathNode{path}, }) } diff --git a/bundle/config/validate/validate_genie_space_etags.go b/bundle/config/validate/validate_genie_space_etags.go index 559d929d8dc..e39c2f00914 100644 --- a/bundle/config/validate/validate_genie_space_etags.go +++ b/bundle/config/validate/validate_genie_space_etags.go @@ -27,10 +27,9 @@ func (v *validateGenieSpaceEtags) Apply(ctx context.Context, b *bundle.Bundle) d if genieSpace.Etag != "" { return diag.Diagnostics{ { - Severity: diag.Error, - Summary: fmt.Sprintf("genie space %q has an etag set. Etags must not be set in bundle configuration", genieSpace.Title), - Paths: structpath.NewPathSlice("resources", "genie_spaces", k), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "genie_spaces", k)), + Severity: diag.Error, + Summary: fmt.Sprintf("genie space %q has an etag set. Etags must not be set in bundle configuration", genieSpace.Title), + Paths: structpath.NewPathSlice("resources", "genie_spaces", k), }, } } diff --git a/bundle/config/validate/validate_job_run_idempotency_token.go b/bundle/config/validate/validate_job_run_idempotency_token.go index 9a6a161941a..ff5dc7f4bd8 100644 --- a/bundle/config/validate/validate_job_run_idempotency_token.go +++ b/bundle/config/validate/validate_job_run_idempotency_token.go @@ -34,10 +34,9 @@ func (v *validateJobRunIdempotencyToken) Apply(_ context.Context, b *bundle.Bund // the run is deleted and break the next deploy. path := structpath.NewPath(nil, "resources", "job_runs", name, "idempotency_token") diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "idempotency_token must not be set in bundle configuration; the CLI sets it on each run-now request", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.NearestLocations(path), + Severity: diag.Error, + Summary: "idempotency_token must not be set in bundle configuration; the CLI sets it on each run-now request", + Paths: []*structpath.PathNode{path}, }) } diff --git a/bundle/config/validate/validate_sync_patterns.go b/bundle/config/validate/validate_sync_patterns.go index e441af87a3f..35dce4ebcdd 100644 --- a/bundle/config/validate/validate_sync_patterns.go +++ b/bundle/config/validate/validate_sync_patterns.go @@ -59,10 +59,9 @@ func checkPatterns(ctx context.Context, patterns []string, path *structpath.Path if len(all) == 0 { path := structpath.NewPath(path, index) logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("Pattern %s does not match any files", pattern), - Locations: b.Config.NearestLocations(path), - Paths: []*structpath.PathNode{path}, + Severity: diag.Warning, + Summary: fmt.Sprintf("Pattern %s does not match any files", pattern), + Paths: []*structpath.PathNode{path}, }) } return nil diff --git a/bundle/config/validate/validate_volume_path.go b/bundle/config/validate/validate_volume_path.go index fe058732684..c1a6e9942d0 100644 --- a/bundle/config/validate/validate_volume_path.go +++ b/bundle/config/validate/validate_volume_path.go @@ -35,11 +35,10 @@ func (m *validateVolumePath) Apply(ctx context.Context, b *bundle.Bundle) diag.D for _, check := range pathChecks { if check.path != "" && strings.HasPrefix(check.path, "/Volumes/") { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("%s %s starts with /Volumes. /Volumes can only be used with workspace.artifact_path.", check.configPath, check.path), - Detail: "For more information, see https://docs.databricks.com/aws/en/dev-tools/bundles/settings#workspace", - Locations: b.Config.NearestLocations(check.configPath), - Paths: []*structpath.PathNode{check.configPath}, + Severity: diag.Error, + Summary: fmt.Sprintf("%s %s starts with /Volumes. /Volumes can only be used with workspace.artifact_path.", check.configPath, check.path), + Detail: "For more information, see https://docs.databricks.com/aws/en/dev-tools/bundles/settings#workspace", + Paths: []*structpath.PathNode{check.configPath}, }) // Return early for root path validation diff --git a/bundle/tests/sync_include_exclude_no_matches_test.go b/bundle/tests/sync_include_exclude_no_matches_test.go index 42938266c5d..aaea07bb236 100644 --- a/bundle/tests/sync_include_exclude_no_matches_test.go +++ b/bundle/tests/sync_include_exclude_no_matches_test.go @@ -25,10 +25,12 @@ func TestSyncIncludeExcludeNoMatchesTest(t *testing.T) { require.Len(t, diags[0].Paths, 1) require.Equal(t, "sync.exclude[0]", diags[0].Paths[0].String()) - assert.Len(t, diags[0].Locations, 1) - require.Equal(t, diags[0].Locations[0].File, path.Join("sync", "override", "databricks.yml")) - require.Equal(t, 17, diags[0].Locations[0].Line) - require.Equal(t, 11, diags[0].Locations[0].Column) + // Locations are filled from Paths by logdiag when the diagnostic is logged. + locations := b.Config.NearestLocations(diags[0].Paths[0]) + assert.Len(t, locations, 1) + require.Equal(t, locations[0].File, path.Join("sync", "override", "databricks.yml")) + require.Equal(t, 17, locations[0].Line) + require.Equal(t, 11, locations[0].Column) summaries := []string{ fmt.Sprintf("Pattern %s does not match any files", path.Join("src", "*")), From 129c83b07f0d6d4159f954851ee2f0e40e4f37dc Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 10:43:17 +0200 Subject: [PATCH 03/14] Report Paths for configuration diagnostics Co-authored-by: Isaac --- .nextchanges/bundles/diag-paths-locations.md | 1 + .../job_runs/on_file_change/output.txt | 3 ++ .../job_runs/on_file_change_errors/output.txt | 7 ++++ .../permissions/clusters/target/output.txt | 2 + .../validate-secret-is-string/output.txt | 1 + .../run_as/empty_run_as_dict/output.txt | 1 + acceptance/bundle/run_as/empty_sp/output.txt | 1 + .../bundle/run_as/empty_user/output.txt | 1 + .../run_as/empty_user_and_sp/output.txt | 1 + .../invalid_both_sp_and_user/output.txt | 1 + .../empty_resources/empty_dict/output.txt | 1 + .../empty_resources/with_grants/output.txt | 1 + .../with_permissions/output.txt | 1 + .../validate/engine-config-valid/output.txt | 2 + .../validate/invalid-engine-bundle/output.txt | 1 + .../validate/invalid-engine-target/output.txt | 1 + .../validate/permissions_overlap/output.txt | 1 + .../presets_name_prefix_dev/output.txt | 1 + .../output.txt | 4 ++ bundle/apps/validate.go | 24 +++++------ .../mutator/resolve_job_run_file_triggers.go | 42 +++++++++---------- .../mutator/resourcemutator/apply_presets.go | 6 +-- .../config/mutator/resourcemutator/run_as.go | 6 +-- .../mutator/resourcemutator/run_as_test.go | 3 +- .../resourcemutator/validate_target_mode.go | 12 +++--- bundle/config/mutator/sync_infer_root.go | 7 ++-- bundle/config/mutator/sync_infer_root_test.go | 4 +- .../mutator/validate_job_run_triggers.go | 24 +++++------ bundle/config/validate/validate_engine.go | 18 ++++---- .../config/validate/validate_engine_test.go | 2 +- bundle/permissions/permission_diagnostics.go | 4 +- 31 files changed, 107 insertions(+), 77 deletions(-) create mode 100644 .nextchanges/bundles/diag-paths-locations.md diff --git a/.nextchanges/bundles/diag-paths-locations.md b/.nextchanges/bundles/diag-paths-locations.md new file mode 100644 index 00000000000..29341476819 --- /dev/null +++ b/.nextchanges/bundles/diag-paths-locations.md @@ -0,0 +1 @@ +* Diagnostics about configuration values now consistently show the configuration path (`at ...`) and its location. diff --git a/acceptance/bundle/resources/job_runs/on_file_change/output.txt b/acceptance/bundle/resources/job_runs/on_file_change/output.txt index cd580a8efa4..6a8321afda9 100644 --- a/acceptance/bundle/resources/job_runs/on_file_change/output.txt +++ b/acceptance/bundle/resources/job_runs/on_file_change/output.txt @@ -115,6 +115,7 @@ Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged === deleting the file warns and re-fires >>> [CLI] bundle plan -o json Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 @@ -168,6 +169,7 @@ Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" } } Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-file-change/default/files... @@ -189,6 +191,7 @@ Resources: 1 created, 0 changed, 1 deleted, 1 unchanged >>> [CLI] bundle plan Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt b/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt index 73f170fe8e9..cc48f824049 100644 --- a/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt +++ b/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt @@ -2,6 +2,7 @@ === a pattern that escapes the sync root is rejected >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: pattern "../shared/*.txt" is not under the sync root + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -15,6 +16,7 @@ Found 1 error === a glob that only matches directories warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "dirs/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -28,6 +30,7 @@ Found 1 warning === a glob matching a directory and an excluded file warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "mixed/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -41,6 +44,7 @@ Found 1 warning === a glob matching only excluded files warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "allexcluded/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -54,6 +58,7 @@ Found 1 warning === a pattern that matches nothing warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "missing/*.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -67,6 +72,7 @@ Found 1 warning === ** is not supported >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: ** in "src/**" is not supported; use * for a single directory level + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -80,6 +86,7 @@ Found 1 error === an absolute pattern is rejected >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: pattern "/tmp/seed.txt" must be relative to the defining YAML file + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Name: job-runs-on-file-change-errors diff --git a/acceptance/bundle/resources/permissions/clusters/target/output.txt b/acceptance/bundle/resources/permissions/clusters/target/output.txt index 37ff60bd025..32f1a555758 100644 --- a/acceptance/bundle/resources/permissions/clusters/target/output.txt +++ b/acceptance/bundle/resources/permissions/clusters/target/output.txt @@ -10,6 +10,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:9:7 { @@ -56,6 +57,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:9:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/files... diff --git a/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt b/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt index 07cfd2978b5..4c525a2b2bc 100644 --- a/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt +++ b/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt @@ -7,6 +7,7 @@ Warning: expected string, found sequence Error: Secret value must be a string at resources.secrets.secret1.value + in databricks.yml:7:7 The secret value for "secret1" must be a string. diff --git a/acceptance/bundle/run_as/empty_run_as_dict/output.txt b/acceptance/bundle/run_as/empty_run_as_dict/output.txt index 88363707b5c..4fc9f617b60 100644 --- a/acceptance/bundle/run_as/empty_run_as_dict/output.txt +++ b/acceptance/bundle/run_as/empty_run_as_dict/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:4:9 "run_as": {}, diff --git a/acceptance/bundle/run_as/empty_sp/output.txt b/acceptance/bundle/run_as/empty_sp/output.txt index 2664376966e..fb87bc34d73 100644 --- a/acceptance/bundle/run_as/empty_sp/output.txt +++ b/acceptance/bundle/run_as/empty_sp/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/empty_user/output.txt b/acceptance/bundle/run_as/empty_user/output.txt index 509819dff0a..9e333cd54bf 100644 --- a/acceptance/bundle/run_as/empty_user/output.txt +++ b/acceptance/bundle/run_as/empty_user/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/empty_user_and_sp/output.txt b/acceptance/bundle/run_as/empty_user_and_sp/output.txt index 0d330606fee..21da625f1fd 100644 --- a/acceptance/bundle/run_as/empty_user_and_sp/output.txt +++ b/acceptance/bundle/run_as/empty_user_and_sp/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt b/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt index 3634b730576..24d52c2a070 100644 --- a/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt +++ b/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:6:3 Name: run_as diff --git a/acceptance/bundle/validate/empty_resources/empty_dict/output.txt b/acceptance/bundle/validate/empty_resources/empty_dict/output.txt index 0ff8111602e..b58917eccad 100644 --- a/acceptance/bundle/validate/empty_resources/empty_dict/output.txt +++ b/acceptance/bundle/validate/empty_resources/empty_dict/output.txt @@ -148,6 +148,7 @@ Warning: required field "name" is not set in databricks.yml:6:12 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:6:12 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/empty_resources/with_grants/output.txt b/acceptance/bundle/validate/empty_resources/with_grants/output.txt index 38c4dc55d19..99ccf3de3ec 100644 --- a/acceptance/bundle/validate/empty_resources/with_grants/output.txt +++ b/acceptance/bundle/validate/empty_resources/with_grants/output.txt @@ -185,6 +185,7 @@ Warning: required field "name" is not set in databricks.yml:7:7 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:7:7 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/empty_resources/with_permissions/output.txt b/acceptance/bundle/validate/empty_resources/with_permissions/output.txt index cef0b18baa3..41b71086fdd 100644 --- a/acceptance/bundle/validate/empty_resources/with_permissions/output.txt +++ b/acceptance/bundle/validate/empty_resources/with_permissions/output.txt @@ -164,6 +164,7 @@ Warning: required field "name" is not set in databricks.yml:7:7 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:7:7 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/engine-config-valid/output.txt b/acceptance/bundle/validate/engine-config-valid/output.txt index e3b138ac1ce..125c9df8a90 100644 --- a/acceptance/bundle/validate/engine-config-valid/output.txt +++ b/acceptance/bundle/validate/engine-config-valid/output.txt @@ -10,6 +10,7 @@ Validation OK! >>> musterr [CLI] bundle validate -t terraform-target Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:10:15 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -22,6 +23,7 @@ Found 1 error >>> musterr [CLI] bundle validate -t terraform-target Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:10:15 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/validate/invalid-engine-bundle/output.txt b/acceptance/bundle/validate/invalid-engine-bundle/output.txt index e76e18b693b..2df5b97577d 100644 --- a/acceptance/bundle/validate/invalid-engine-bundle/output.txt +++ b/acceptance/bundle/validate/invalid-engine-bundle/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle validate Error: invalid value "invalid_engine" for bundle.engine (expected "direct") + at bundle.engine in databricks.yml:3:11 Name: test-engine-config diff --git a/acceptance/bundle/validate/invalid-engine-target/output.txt b/acceptance/bundle/validate/invalid-engine-target/output.txt index c44d9dea7b6..af65b8f85ee 100644 --- a/acceptance/bundle/validate/invalid-engine-target/output.txt +++ b/acceptance/bundle/validate/invalid-engine-target/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle validate Error: invalid value "invalid_engine" for bundle.engine (expected "direct") + at bundle.engine in databricks.yml:7:15 Name: test-engine-config diff --git a/acceptance/bundle/validate/permissions_overlap/output.txt b/acceptance/bundle/validate/permissions_overlap/output.txt index 43b95ddd440..c2086552f5d 100644 --- a/acceptance/bundle/validate/permissions_overlap/output.txt +++ b/acceptance/bundle/validate/permissions_overlap/output.txt @@ -10,6 +10,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 { diff --git a/acceptance/bundle/validate/presets_name_prefix_dev/output.txt b/acceptance/bundle/validate/presets_name_prefix_dev/output.txt index c7d93c71444..76b53f1a7db 100644 --- a/acceptance/bundle/validate/presets_name_prefix_dev/output.txt +++ b/acceptance/bundle/validate/presets_name_prefix_dev/output.txt @@ -12,6 +12,7 @@ presets: # PRESETS >>> musterr [CLI] bundle validate -o json Error: prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development' + at presets.name_prefix in databricks.yml:25:16 diff --git a/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt b/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt index eec035d2671..bed48e44963 100644 --- a/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt +++ b/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt @@ -2,9 +2,11 @@ >>> [CLI] bundle validate Warning: invalid value "12" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[0].level + in databricks.yml:7:7 Warning: invalid value "fuzz_badlevel" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[1].level + in databricks.yml:7:7 Name: test-bundle Target: default @@ -17,9 +19,11 @@ Found 2 warnings >>> [CLI] bundle validate --strict Warning: invalid value "12" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[0].level + in databricks.yml:7:7 Warning: invalid value "fuzz_badlevel" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[1].level + in databricks.yml:7:7 Name: test-bundle Target: default diff --git a/bundle/apps/validate.go b/bundle/apps/validate.go index cd88d0dc39b..f27203075ef 100644 --- a/bundle/apps/validate.go +++ b/bundle/apps/validate.go @@ -25,30 +25,30 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics for key, app := range b.Config.Resources.Apps { if app.SourceCodePath == "" && app.GitSource == nil { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Missing app source code path or git source", - Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key)), + Severity: diag.Error, + Summary: "Missing app source code path or git source", + Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key), + Paths: structpath.NewPathSlice("resources", "apps", key), }) continue } if app.SourceCodePath != "" && app.GitSource != nil { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Both source_code_path and git_source fields are set", - Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key)), + Severity: diag.Error, + Summary: "Both source_code_path and git_source fields are set", + Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key), + Paths: structpath.NewPathSlice("resources", "apps", key), }) continue } if _, ok := usedSourceCodePaths[app.SourceCodePath]; ok { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Duplicate app source code path", - Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]), - Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key, "source_code_path")), + Severity: diag.Error, + Summary: "Duplicate app source code path", + Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]), + Paths: structpath.NewPathSlice("resources", "apps", key, "source_code_path"), }) } usedSourceCodePaths[app.SourceCodePath] = key diff --git a/bundle/config/mutator/resolve_job_run_file_triggers.go b/bundle/config/mutator/resolve_job_run_file_triggers.go index 8b952bae235..83fc50711d5 100644 --- a/bundle/config/mutator/resolve_job_run_file_triggers.go +++ b/bundle/config/mutator/resolve_job_run_file_triggers.go @@ -118,9 +118,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri matched, err := pathlib.Match(relPattern, rel) if err != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), + Paths: []*structpath.PathNode{loc}, }) continue } @@ -130,9 +130,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri hash, err := hashFile(b.SyncRoot, rel) if err != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("hash %q: %s", rel, err), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("hash %q: %s", rel, err), + Paths: []*structpath.PathNode{loc}, }) continue } @@ -144,9 +144,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri } if matches == 0 && !diags.HasError() { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fileTriggerPrefix + fmt.Sprintf("no synced files match %q", pattern), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Warning, + Summary: fileTriggerPrefix + fmt.Sprintf("no synced files match %q", pattern), + Paths: []*structpath.PathNode{loc}, }) } return relPattern, hex.EncodeToString(h.Sum(nil)), diags @@ -157,9 +157,9 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt // A double star looks recursive but path.Match treats it as two ordinary stars. if strings.Contains(pattern, "**") { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("** in %q is not supported; use * for a single directory level", pattern), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("** in %q is not supported; use * for a single directory level", pattern), + Paths: []*structpath.PathNode{loc}, }) } // Reject a genuinely absolute path; Join would otherwise silently reinterpret it @@ -169,9 +169,9 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt // through to the sync-root containment check below. if filepath.IsAbs(pattern) || pathlib.IsAbs(pattern) { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q must be relative to the defining YAML file", pattern), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q must be relative to the defining YAML file", pattern), + Paths: []*structpath.PathNode{loc}, }) } // NormalizePaths has already rewritten YAML-relative globs to be bundle-root @@ -181,18 +181,18 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt relPattern, err := filepath.Rel(b.SyncRootPath, joined) if err != nil || !filepath.IsLocal(relPattern) { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q is not under the sync root", pattern), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q is not under the sync root", pattern), + Paths: []*structpath.PathNode{loc}, }) } relPattern = filepath.ToSlash(relPattern) _, err = pathlib.Match(relPattern, "") if err != nil { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), - Locations: b.Config.NearestLocations(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), + Paths: []*structpath.PathNode{loc}, }) } return relPattern, diags diff --git a/bundle/config/mutator/resourcemutator/apply_presets.go b/bundle/config/mutator/resourcemutator/apply_presets.go index 9bac80ee5ed..29ef6841d15 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets.go +++ b/bundle/config/mutator/resourcemutator/apply_presets.go @@ -351,9 +351,9 @@ func validatePauseStatus(b *bundle.Bundle) diag.Diagnostics { return nil } return diag.Diagnostics{{ - Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED", - Severity: diag.Error, - Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "trigger_pause_status"))}, + Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED", + Severity: diag.Error, + Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), }} } diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index df852ee75b7..351f5ec8c6a 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -55,9 +55,9 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics { } if identityCount != 1 { return diag.Diagnostics{{ - Summary: "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name", - Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "run_as"))}, - Severity: diag.Error, + Summary: "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name", + Paths: structpath.NewPathSlice("run_as"), + Severity: diag.Error, }} } diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index cfb98cbb84c..da8c2d4941d 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -10,7 +10,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" @@ -384,7 +383,7 @@ func TestRunAsIdentities(t *testing.T) { diags = bundle.Apply(t.Context(), b, SetRunAs()) if tc.wantError { require.ErrorContains(t, diags.Error(), "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name") - assert.Equal(t, []diag.Location{r.NearestLocation(structpath.NewPath(nil, "run_as"))}, diags[0].Locations) + assert.Equal(t, "run_as", diags[0].Paths[0].String()) } else { require.NoError(t, diags.Error()) } diff --git a/bundle/config/mutator/resourcemutator/validate_target_mode.go b/bundle/config/mutator/resourcemutator/validate_target_mode.go index 812d4df66fd..110f4c9f9a2 100644 --- a/bundle/config/mutator/resourcemutator/validate_target_mode.go +++ b/bundle/config/mutator/resourcemutator/validate_target_mode.go @@ -54,9 +54,9 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { // historically allowed.) if p.TriggerPauseStatus == config.Unpaused { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", - Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "trigger_pause_status"))}, + Severity: diag.Error, + Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", + Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), }) } @@ -76,9 +76,9 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { // it's a pitfall for users if they don't include it and later find out that // only a single user can do development deployments. diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", - Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "name_prefix"))}, + Severity: diag.Error, + Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", + Paths: structpath.NewPathSlice("presets", "name_prefix"), }) } return diags diff --git a/bundle/config/mutator/sync_infer_root.go b/bundle/config/mutator/sync_infer_root.go index d8d4a3f6266..7783bc10088 100644 --- a/bundle/config/mutator/sync_infer_root.go +++ b/bundle/config/mutator/sync_infer_root.go @@ -92,10 +92,9 @@ func (m *syncInferRoot) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno syncPath := structpath.NewPath(nil, "sync", "paths", i) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("invalid sync path %q", path), - Locations: b.Config.NearestLocations(syncPath), - Paths: []*structpath.PathNode{syncPath}, + Severity: diag.Error, + Summary: fmt.Sprintf("invalid sync path %q", path), + Paths: []*structpath.PathNode{syncPath}, }) } diff --git a/bundle/config/mutator/sync_infer_root_test.go b/bundle/config/mutator/sync_infer_root_test.go index e34b4210bd6..c9c65a384b7 100644 --- a/bundle/config/mutator/sync_infer_root_test.go +++ b/bundle/config/mutator/sync_infer_root_test.go @@ -190,9 +190,9 @@ func TestSyncInferRoot_Error(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SyncInferRoot()) require.Len(t, diags, 2) assert.Equal(t, `invalid sync path "../../../../error"`, diags[0].Summary) - assert.Equal(t, "databricks.yml:0:0", diags[0].Locations[0].String()) + assert.Equal(t, "databricks.yml:0:0", b.Config.NearestLocations(diags[0].Paths[0])[0].String()) assert.Equal(t, "sync.paths[0]", diags[0].Paths[0].String()) assert.Equal(t, `invalid sync path "../../../../../error"`, diags[1].Summary) - assert.Equal(t, "databricks.yml:0:0", diags[1].Locations[0].String()) + assert.Equal(t, "databricks.yml:0:0", b.Config.NearestLocations(diags[1].Paths[0])[0].String()) assert.Equal(t, "sync.paths[2]", diags[1].Paths[0].String()) } diff --git a/bundle/config/mutator/validate_job_run_triggers.go b/bundle/config/mutator/validate_job_run_triggers.go index 7d09a50fc6d..3579d3f6281 100644 --- a/bundle/config/mutator/validate_job_run_triggers.go +++ b/bundle/config/mutator/validate_job_run_triggers.go @@ -30,34 +30,34 @@ func (*validateJobRunTriggers) Apply(_ context.Context, b *bundle.Bundle) diag.D path := structpath.NewPath(nil, "resources", "job_runs", name, "lifecycle", "triggers", i) if t.OnBundleDeploy == nil && t.OnFileChange == nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers entry must set on_bundle_deploy or on_file_change", - Locations: b.Config.NearestLocations(path), + Severity: diag.Error, + Summary: "lifecycle.triggers entry must set on_bundle_deploy or on_file_change", + Paths: []*structpath.PathNode{path}, }) continue } if t.OnBundleDeploy != nil && t.OnFileChange != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers entry must set only one of on_bundle_deploy or on_file_change", - Locations: b.Config.NearestLocations(path), + Severity: diag.Error, + Summary: "lifecycle.triggers entry must set only one of on_bundle_deploy or on_file_change", + Paths: []*structpath.PathNode{path}, }) continue } if t.OnBundleDeploy != nil && !*t.OnBundleDeploy { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers.on_bundle_deploy must be true when set", - Locations: b.Config.NearestLocations(structpath.NewPath(path, "on_bundle_deploy")), + Severity: diag.Error, + Summary: "lifecycle.triggers.on_bundle_deploy must be true when set", + Paths: []*structpath.PathNode{structpath.NewPath(path, "on_bundle_deploy")}, }) } if t.OnFileChange != nil { onFileChange := structpath.NewPath(path, "on_file_change") if strings.TrimSpace(*t.OnFileChange) == "" { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers.on_file_change must be non-empty when set", - Locations: b.Config.NearestLocations(onFileChange), + Severity: diag.Error, + Summary: "lifecycle.triggers.on_file_change must be non-empty when set", + Paths: []*structpath.PathNode{onFileChange}, }) continue } diff --git a/bundle/config/validate/validate_engine.go b/bundle/config/validate/validate_engine.go index f04409924d8..20097d8ad8b 100644 --- a/bundle/config/validate/validate_engine.go +++ b/bundle/config/validate/validate_engine.go @@ -40,14 +40,12 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos return diags } - loc := b.Config.NearestLocation(structpath.NewPath(nil, "bundle", "engine")) - parsed, ok := engine.Parse(string(configEngine)) if !ok { return diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), - Locations: []diag.Location{loc}, + Severity: diag.Error, + Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), + Paths: structpath.NewPathSlice("bundle", "engine"), }) } @@ -57,10 +55,12 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos severity = diag.Warning } return diags.Append(diag.Diagnostic{ - Severity: severity, - Summary: engine.TerraformRemovedSummary, - Detail: engine.TerraformRemovedConfigDetail, - Locations: []diag.Location{loc}, + Severity: severity, + Summary: engine.TerraformRemovedSummary, + Detail: engine.TerraformRemovedConfigDetail, + Paths: structpath.NewPathSlice("bundle", "engine"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "bundle", "engine"))}, }) } diff --git a/bundle/config/validate/validate_engine_test.go b/bundle/config/validate/validate_engine_test.go index da1bd7bf12d..4e4c49c3ca7 100644 --- a/bundle/config/validate/validate_engine_test.go +++ b/bundle/config/validate/validate_engine_test.go @@ -38,7 +38,7 @@ func TestValidateEngineTerraformRemoved(t *testing.T) { assert.Len(t, diags, 1) assert.Equal(t, diag.Error, diags[0].Severity) assert.Contains(t, diags[0].Summary, "has been removed") - assert.Equal(t, []diag.Location{loc}, diags[0].Locations) + assert.Equal(t, "bundle.engine", diags[0].Paths[0].String()) } func TestValidateEngineNotSet(t *testing.T) { diff --git a/bundle/permissions/permission_diagnostics.go b/bundle/permissions/permission_diagnostics.go index 8d4aa37a703..55437b5b57d 100644 --- a/bundle/permissions/permission_diagnostics.go +++ b/bundle/permissions/permission_diagnostics.go @@ -59,8 +59,8 @@ func (m *permissionDiagnostics) Apply(ctx context.Context, b *bundle.Bundle) dia identityType, b.Config.Workspace.CurrentUser.UserName, ), - Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "permissions"))}, - ID: diag.PermissionNotIncluded, + Paths: structpath.NewPathSlice("permissions"), + ID: diag.PermissionNotIncluded, }} } From 559578b3d64ce57a9c6c5ea4f9c52d60a3c7a2f4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 10:49:31 +0200 Subject: [PATCH 04/14] Drop Locations that duplicate Paths for value locations Co-authored-by: Isaac --- bundle/config/mutator/compute_id_compat.go | 7 ++- bundle/config/mutator/load_dbalert_files.go | 44 +++++++++---------- .../mutator/rewrite_workspace_prefix.go | 9 ++-- .../validate_secret_value_is_variable.go | 21 ++++----- .../validate/all_resources_have_values.go | 7 ++- bundle/config/validate/enum.go | 7 ++- .../validate/interpolation_in_auth_config.go | 3 +- .../no_variable_reference_in_resource_key.go | 7 ++- bundle/config/validate/required.go | 21 ++++----- bundle/config/validate/scripts.go | 26 +++++------ 10 files changed, 67 insertions(+), 85 deletions(-) diff --git a/bundle/config/mutator/compute_id_compat.go b/bundle/config/mutator/compute_id_compat.go index 297769503b4..833b700228e 100644 --- a/bundle/config/mutator/compute_id_compat.go +++ b/bundle/config/mutator/compute_id_compat.go @@ -57,10 +57,9 @@ func rewriteComputeIdToClusterId(b *bundle.Bundle, p *structpath.PathNode, v str } diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: "compute_id is deprecated, please use cluster_id instead", - Locations: computeId.Locations(), - Paths: []*structpath.PathNode{structpath.NewStringKey(p, "compute_id")}, + Severity: diag.Warning, + Summary: "compute_id is deprecated, please use cluster_id instead", + Paths: []*structpath.PathNode{structpath.NewStringKey(p, "compute_id")}, }) err := b.Config.Assign(structpath.NewStringKey(p, "cluster_id"), computeId) diff --git a/bundle/config/mutator/load_dbalert_files.go b/bundle/config/mutator/load_dbalert_files.go index b6914230731..db7611d6d3d 100644 --- a/bundle/config/mutator/load_dbalert_files.go +++ b/bundle/config/mutator/load_dbalert_files.go @@ -76,12 +76,11 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia return diag.Diagnostics{ { - ID: "", - Severity: diag.Error, - Summary: fmt.Sprintf("field %s is not allowed in the bundle configuration.", k), - Detail: "When a .dbalert.json is specified, only the following fields are allowed in the bundle configuration: " + strings.Join(allowedInYAML, ", "), - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, k)}, - Locations: v.Locations(), + ID: "", + Severity: diag.Error, + Summary: fmt.Sprintf("field %s is not allowed in the bundle configuration.", k), + Detail: "When a .dbalert.json is specified, only the following fields are allowed in the bundle configuration: " + strings.Join(allowedInYAML, ", "), + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, k)}, }, } } @@ -98,12 +97,11 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if err != nil { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf("failed to read .dbalert.json file %s: %s", alert.FilePath, err), - Detail: "", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf("failed to read .dbalert.json file %s: %s", alert.FilePath, err), + Detail: "", + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } @@ -113,12 +111,11 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if err != nil { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf("failed to parse .dbalert.json file %s: %s", alert.FilePath, err), - Detail: "", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf("failed to parse .dbalert.json file %s: %s", alert.FilePath, err), + Detail: "", + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } @@ -127,12 +124,11 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if structvar.ContainsVariableReference(string(content)) { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf(".alert file %s must not contain variable interpolations.", alert.FilePath), - Detail: "Please inline the alert configuration in the bundle configuration to use variables", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf(".alert file %s must not contain variable interpolations.", alert.FilePath), + Detail: "Please inline the alert configuration in the bundle configuration to use variables", + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } diff --git a/bundle/config/mutator/rewrite_workspace_prefix.go b/bundle/config/mutator/rewrite_workspace_prefix.go index 390a13bb490..e194086fac0 100644 --- a/bundle/config/mutator/rewrite_workspace_prefix.go +++ b/bundle/config/mutator/rewrite_workspace_prefix.go @@ -61,11 +61,10 @@ func (m *rewriteWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) di newPath = strings.ReplaceAll(newPath, rewrite.pattern, rewrite.replacement) diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", rewrite.pattern, vv, newPath), - Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths", - Locations: v.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Warning, + Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", rewrite.pattern, vv, newPath), + Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths", + Paths: []*structpath.PathNode{p}, }) } diff --git a/bundle/config/mutator/validate_secret_value_is_variable.go b/bundle/config/mutator/validate_secret_value_is_variable.go index 0b14e37f4c4..4fae7c443ae 100644 --- a/bundle/config/mutator/validate_secret_value_is_variable.go +++ b/bundle/config/mutator/validate_secret_value_is_variable.go @@ -29,11 +29,10 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun val := b.Config.View().Lookup(p) if !val.IsValid() { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Secret value must be a string", - Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: "Secret value must be a string", + Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), + Paths: []*structpath.PathNode{p}, }) continue } @@ -41,11 +40,10 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun valueStr, ok := val.AsString() if !ok { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Secret value must be a string", - Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: "Secret value must be a string", + Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), + Paths: []*structpath.PathNode{p}, }) continue } @@ -58,8 +56,7 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun Detail: fmt.Sprintf(`The secret value for "%s" must be a variable reference (e.g., ${var.my_secret}). Plain text secret values are not allowed to prevent leaking secrets in configuration files. Use bundle variables to pass secret values at deployment time.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Paths: []*structpath.PathNode{p}, }) continue } diff --git a/bundle/config/validate/all_resources_have_values.go b/bundle/config/validate/all_resources_have_values.go index 434fdba513a..2ad149b9145 100644 --- a/bundle/config/validate/all_resources_have_values.go +++ b/bundle/config/validate/all_resources_have_values.go @@ -39,10 +39,9 @@ func (m *allResourcesHaveValues) Apply(ctx context.Context, b *bundle.Bundle) di p := structpath.NewPath(nil, "resources", group.Description.PluralName, rName) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("%s %s is not defined", rType, rName), - Locations: b.Config.ExactLocations(p), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: fmt.Sprintf("%s %s is not defined", rType, rName), + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/enum.go b/bundle/config/validate/enum.go index 392b431c50c..4db10917ae0 100644 --- a/bundle/config/validate/enum.go +++ b/bundle/config/validate/enum.go @@ -61,10 +61,9 @@ func (f *enum) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { if !validValue { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("invalid value %q for enum field. Valid values are %v", strValue, validValues), - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Warning, + Summary: fmt.Sprintf("invalid value %q for enum field. Valid values are %v", strValue, validValues), + Paths: []*structpath.PathNode{np}, }) } diff --git a/bundle/config/validate/interpolation_in_auth_config.go b/bundle/config/validate/interpolation_in_auth_config.go index a282b72790e..3e7999f0551 100644 --- a/bundle/config/validate/interpolation_in_auth_config.go +++ b/bundle/config/validate/interpolation_in_auth_config.go @@ -74,8 +74,7 @@ func (f *noInterpolationInAuthConfig) Apply(ctx context.Context, b *bundle.Bundl Summary: "Variable interpolation is not supported for fields that configure authentication", Detail: fmt.Sprintf(`Interpolation is not supported for the field %s. Please set the %s environment variable if you wish to configure this field at runtime.`, p.String(), envVar), - Locations: v.Locations(), - Paths: []*structpath.PathNode{p}, + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/no_variable_reference_in_resource_key.go b/bundle/config/validate/no_variable_reference_in_resource_key.go index 9f568f2262d..7000ecb60c4 100644 --- a/bundle/config/validate/no_variable_reference_in_resource_key.go +++ b/bundle/config/validate/no_variable_reference_in_resource_key.go @@ -36,10 +36,9 @@ func (m *noVariableReferenceInResourceKey) Apply(_ context.Context, b *bundle.Bu } p := structpath.NewPath(prefix, group.Description.PluralName, key) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("resource key %q must not contain variable references", key), - Locations: b.Config.ExactLocations(p), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: fmt.Sprintf("resource key %q must not contain variable references", key), + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/required.go b/bundle/config/validate/required.go index 8f94d4b17c8..e894fbcdeb2 100644 --- a/bundle/config/validate/required.go +++ b/bundle/config/validate/required.go @@ -48,10 +48,9 @@ func warnForMissingFields(ctx context.Context, b *bundle.Bundle) diag.Diagnostic vv := v.Get(field) if vv.Kind() == structvar.KindInvalid || vv.Kind() == structvar.KindNil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("required field %q is not set", field), - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Warning, + Summary: fmt.Sprintf("required field %q is not set", field), + Paths: []*structpath.PathNode{np}, }) } } @@ -155,18 +154,16 @@ func errorForInvalidGrants(ctx context.Context, b *bundle.Bundle) diag.Diagnosti func(np *structpath.PathNode, v structvar.View) error { if isMissingOrEmptyString(v.Get("principal")) { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "grant principal is required", - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Error, + Summary: "grant principal is required", + Paths: []*structpath.PathNode{np}, }) } if isMissingOrEmptySequence(v.Get("privileges")) { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "grant privileges is required", - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Error, + Summary: "grant privileges is required", + Paths: []*structpath.PathNode{np}, }) } return nil diff --git a/bundle/config/validate/scripts.go b/bundle/config/validate/scripts.go index 0b71a27edc7..48b5ed0e7ee 100644 --- a/bundle/config/validate/scripts.go +++ b/bundle/config/validate/scripts.go @@ -70,16 +70,15 @@ func validateScriptContent(b *bundle.Bundle, key, content string, p *structpath. Severity: diag.Error, Summary: fmt.Sprintf("Found %s in script %s.content. Interpolation syntax ${...} is not supported in script content", first, key), Detail: `The ${...} syntax is not allowed in script content. To use a bundle value, -declare an environment variable in the script's "env:" section and reference it -from "content" with $NAME: - - scripts: - ` + key + `: - env: - MY_VAR: ${var.foo} - content: echo "$MY_VAR"`, - Locations: b.Config.ExactLocations(p), - Paths: []*structpath.PathNode{p}, + declare an environment variable in the script's "env:" section and reference it + from "content" with $NAME: + + scripts: + ` + key + `: + env: + MY_VAR: ${var.foo} + content: echo "$MY_VAR"`, + Paths: []*structpath.PathNode{p}, }} } @@ -100,10 +99,9 @@ func validateScriptEnv(b *bundle.Bundle, key string, env map[string]string) diag continue } diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("${%s} cannot be used in scripts.%s.env.%s; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute", refPath, key, name), - Locations: b.Config.ExactLocations(envValuePath), - Paths: []*structpath.PathNode{envValuePath}, + Severity: diag.Error, + Summary: fmt.Sprintf("${%s} cannot be used in scripts.%s.env.%s; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute", refPath, key, name), + Paths: []*structpath.PathNode{envValuePath}, }) } } From 24563042cc622a1e4dd9563c5253aee56a8cfa2f Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 11:09:16 +0200 Subject: [PATCH 05/14] Sort validation diagnostics by path before locations Locations are filled in from paths when the diagnostics are logged, after sorting, so order by path (deterministic) and keep locations as a tie breaker. Co-authored-by: Isaac --- .../secret_scope_required_principal/output.txt | 16 ++++++++-------- .../bundle/validate/volume_defaults/output.txt | 12 ++++++------ bundle/config/validate/enum.go | 12 +----------- bundle/config/validate/required.go | 9 ++++----- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/acceptance/bundle/validate/secret_scope_required_principal/output.txt b/acceptance/bundle/validate/secret_scope_required_principal/output.txt index 987ce601cb6..99d2359f04a 100644 --- a/acceptance/bundle/validate/secret_scope_required_principal/output.txt +++ b/acceptance/bundle/validate/secret_scope_required_principal/output.txt @@ -11,14 +11,14 @@ Error: secret scope permission principal is required Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.wrong_type_principal.permissions[0] - in databricks.yml:18:7 + at resources.secret_scopes.no_principal.permissions[0] + in databricks.yml:7:7 Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.no_principal.permissions[0] - in databricks.yml:7:7 + at resources.secret_scopes.wrong_type_principal.permissions[0] + in databricks.yml:18:7 Set one of user_name, group_name or service_principal_name @@ -42,14 +42,14 @@ Error: secret scope permission principal is required Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.wrong_type_principal.permissions[0] - in databricks.yml:18:7 + at resources.secret_scopes.no_principal.permissions[0] + in databricks.yml:7:7 Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.no_principal.permissions[0] - in databricks.yml:7:7 + at resources.secret_scopes.wrong_type_principal.permissions[0] + in databricks.yml:18:7 Set one of user_name, group_name or service_principal_name diff --git a/acceptance/bundle/validate/volume_defaults/output.txt b/acceptance/bundle/validate/volume_defaults/output.txt index ecdbd7ae7b7..8de98a74537 100644 --- a/acceptance/bundle/validate/volume_defaults/output.txt +++ b/acceptance/bundle/validate/volume_defaults/output.txt @@ -1,3 +1,7 @@ +Warning: required field "catalog_name" is not set + at resources.volumes.v1 + in databricks.yml:7:7 + Warning: required field "catalog_name" is not set at resources.volumes.v2 in databricks.yml:10:7 @@ -6,7 +10,7 @@ Warning: required field "catalog_name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "catalog_name" is not set +Warning: required field "name" is not set at resources.volumes.v1 in databricks.yml:7:7 @@ -18,7 +22,7 @@ Warning: required field "name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "name" is not set +Warning: required field "schema_name" is not set at resources.volumes.v1 in databricks.yml:7:7 @@ -30,10 +34,6 @@ Warning: required field "schema_name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "schema_name" is not set - at resources.volumes.v1 - in databricks.yml:7:7 - Warning: invalid value "" for enum field. Valid values are [EXTERNAL MANAGED] at resources.volumes.v1.volume_type in databricks.yml:7:20 diff --git a/bundle/config/validate/enum.go b/bundle/config/validate/enum.go index 4db10917ae0..11fa2a22edf 100644 --- a/bundle/config/validate/enum.go +++ b/bundle/config/validate/enum.go @@ -1,7 +1,6 @@ package validate import ( - "cmp" "context" "fmt" "maps" @@ -73,16 +72,7 @@ func (f *enum) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { return diag.FromErr(err) } - // Sort diagnostics to make them deterministic - slices.SortFunc(diags, func(a, b diag.Diagnostic) int { - // First sort by summary - if n := cmp.Compare(a.Summary, b.Summary); n != 0 { - return n - } - - // Then sort by locations as a tie breaker if summaries are the same. - return cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)) - }) + sortDiagnostics(diags) return diags } diff --git a/bundle/config/validate/required.go b/bundle/config/validate/required.go index e894fbcdeb2..7e73b4ecabd 100644 --- a/bundle/config/validate/required.go +++ b/bundle/config/validate/required.go @@ -74,13 +74,12 @@ func sortDiagnostics(diags diag.Diagnostics) { return n } - // Then sort by locations as a tie breaker if summaries are the same. - if n := cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)); n != 0 { + // Then by path: locations are filled in from paths only when the diagnostics are + // logged, after sorting. + if n := cmp.Compare(fmt.Sprintf("%v", a.Paths), fmt.Sprintf("%v", b.Paths)); n != 0 { return n } - - // Sibling entries can share a location; fall back to path for a stable order. - return cmp.Compare(fmt.Sprintf("%v", a.Paths), fmt.Sprintf("%v", b.Paths)) + return cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)) }) } From 9d0cbe5668c37d2735f103af8639c42306da6b6a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 11:12:59 +0200 Subject: [PATCH 06/14] Changelog link Co-authored-by: Isaac --- .nextchanges/bundles/6999.md | 1 + .nextchanges/bundles/diag-paths-locations.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .nextchanges/bundles/6999.md delete mode 100644 .nextchanges/bundles/diag-paths-locations.md diff --git a/.nextchanges/bundles/6999.md b/.nextchanges/bundles/6999.md new file mode 100644 index 00000000000..fd67b05254e --- /dev/null +++ b/.nextchanges/bundles/6999.md @@ -0,0 +1 @@ +* Diagnostics about configuration values now consistently show the configuration path (`at ...`) and its location. ([#6999](https://github.com/databricks/cli/pull/6999)) diff --git a/.nextchanges/bundles/diag-paths-locations.md b/.nextchanges/bundles/diag-paths-locations.md deleted file mode 100644 index 29341476819..00000000000 --- a/.nextchanges/bundles/diag-paths-locations.md +++ /dev/null @@ -1 +0,0 @@ -* Diagnostics about configuration values now consistently show the configuration path (`at ...`) and its location. From db23ae7978d16a46beafdfa23d7ccc378f04396e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 11:18:06 +0200 Subject: [PATCH 07/14] Keep exhaustruct field lists in load_dbalert_files Co-authored-by: Isaac --- bundle/config/mutator/load_dbalert_files.go | 44 +++++++++++---------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/bundle/config/mutator/load_dbalert_files.go b/bundle/config/mutator/load_dbalert_files.go index db7611d6d3d..517cc5a3668 100644 --- a/bundle/config/mutator/load_dbalert_files.go +++ b/bundle/config/mutator/load_dbalert_files.go @@ -76,11 +76,12 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia return diag.Diagnostics{ { - ID: "", - Severity: diag.Error, - Summary: fmt.Sprintf("field %s is not allowed in the bundle configuration.", k), - Detail: "When a .dbalert.json is specified, only the following fields are allowed in the bundle configuration: " + strings.Join(allowedInYAML, ", "), - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, k)}, + ID: "", + Severity: diag.Error, + Summary: fmt.Sprintf("field %s is not allowed in the bundle configuration.", k), + Detail: "When a .dbalert.json is specified, only the following fields are allowed in the bundle configuration: " + strings.Join(allowedInYAML, ", "), + Locations: nil, + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, k)}, }, } } @@ -97,11 +98,12 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if err != nil { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf("failed to read .dbalert.json file %s: %s", alert.FilePath, err), - Detail: "", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf("failed to read .dbalert.json file %s: %s", alert.FilePath, err), + Detail: "", + Locations: nil, + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } @@ -111,11 +113,12 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if err != nil { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf("failed to parse .dbalert.json file %s: %s", alert.FilePath, err), - Detail: "", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf("failed to parse .dbalert.json file %s: %s", alert.FilePath, err), + Detail: "", + Locations: nil, + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } @@ -124,11 +127,12 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia if structvar.ContainsVariableReference(string(content)) { return diag.Diagnostics{ { - ID: diag.ID(""), - Severity: diag.Error, - Summary: fmt.Sprintf(".alert file %s must not contain variable interpolations.", alert.FilePath), - Detail: "Please inline the alert configuration in the bundle configuration to use variables", - Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, + ID: diag.ID(""), + Severity: diag.Error, + Summary: fmt.Sprintf(".alert file %s must not contain variable interpolations.", alert.FilePath), + Detail: "Please inline the alert configuration in the bundle configuration to use variables", + Locations: nil, + Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, }, } } From 27a4415c8998d25d30a8aebedb3c1d36adc59d8a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 11:33:21 +0200 Subject: [PATCH 08/14] Regenerate goldens with the new at/in lines; slash-normalize a test location Co-authored-by: Isaac --- .../bundle/deploy/immutable-permissions-change/output.txt | 2 ++ acceptance/bundle/migrate/auto/default/output.txt | 2 ++ .../migrate/command/engine-config-terraform/output.txt | 1 + acceptance/bundle/migrate/command/runas/output.txt | 5 +++++ acceptance/bundle/presets/preset_vs_dev_mode/output.txt | 1 + acceptance/bundle/run/scripts/no_content/output.txt | 4 ++++ acceptance/bundle/summary/engine-terraform/output.txt | 4 ++++ .../telemetry/deploy-workspace-folder-permissions/output.txt | 2 ++ bundle/tests/sync_include_exclude_no_matches_test.go | 3 ++- 9 files changed, 23 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/deploy/immutable-permissions-change/output.txt b/acceptance/bundle/deploy/immutable-permissions-change/output.txt index 7e45089b269..59f94e0e194 100644 --- a/acceptance/bundle/deploy/immutable-permissions-change/output.txt +++ b/acceptance/bundle/deploy/immutable-permissions-change/output.txt @@ -19,6 +19,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:22:3 Recreated internal_immutable_snapshots.immutable @@ -42,6 +43,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:22:3 The following resources will be deleted: diff --git a/acceptance/bundle/migrate/auto/default/output.txt b/acceptance/bundle/migrate/auto/default/output.txt index 30a5dd774e6..c7da628a3f6 100644 --- a/acceptance/bundle/migrate/auto/default/output.txt +++ b/acceptance/bundle/migrate/auto/default/output.txt @@ -7,6 +7,7 @@ >>> [CLI] bundle summary Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -26,6 +27,7 @@ Resources: >>> [CLI] bundle summary -o json Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/migrate/command/engine-config-terraform/output.txt b/acceptance/bundle/migrate/command/engine-config-terraform/output.txt index e2456dba2d6..46f57519fc5 100644 --- a/acceptance/bundle/migrate/command/engine-config-terraform/output.txt +++ b/acceptance/bundle/migrate/command/engine-config-terraform/output.txt @@ -1,6 +1,7 @@ >>> musterr [CLI] bundle deployment migrate Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/migrate/command/runas/output.txt b/acceptance/bundle/migrate/command/runas/output.txt index c17ffdec425..4de05f1f9ea 100644 --- a/acceptance/bundle/migrate/command/runas/output.txt +++ b/acceptance/bundle/migrate/command/runas/output.txt @@ -14,6 +14,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Notice: migrating your bundle to direct deployment engine (https://github.com/databricks/cli/issues/6765). @@ -31,6 +32,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Success! Migrated 2 resources to direct engine state file: [TEST_TMP_DIR]/.databricks/bundle/production/resources.json @@ -55,6 +57,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged @@ -70,6 +73,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 @@ -84,6 +88,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 The following resources will be deleted: diff --git a/acceptance/bundle/presets/preset_vs_dev_mode/output.txt b/acceptance/bundle/presets/preset_vs_dev_mode/output.txt index 9e4e62dc935..1f44f4743fb 100644 --- a/acceptance/bundle/presets/preset_vs_dev_mode/output.txt +++ b/acceptance/bundle/presets/preset_vs_dev_mode/output.txt @@ -1,4 +1,5 @@ Error: target with 'mode: development' cannot set trigger pause status to UNPAUSED by default + at presets.trigger_pause_status in databricks.yml:9:29 Name: preset diff --git a/acceptance/bundle/run/scripts/no_content/output.txt b/acceptance/bundle/run/scripts/no_content/output.txt index f56be04cac9..8ade3094f0a 100644 --- a/acceptance/bundle/run/scripts/no_content/output.txt +++ b/acceptance/bundle/run/scripts/no_content/output.txt @@ -2,9 +2,11 @@ >>> [CLI] bundle run foo Error: Script bar has no content at scripts.bar.content + in databricks.yml:7:14 Error: Script foo has no content at scripts.foo.content + in databricks.yml:5:7 Exit code: 1 @@ -12,9 +14,11 @@ Exit code: 1 >>> [CLI] bundle run bar Error: Script bar has no content at scripts.bar.content + in databricks.yml:7:14 Error: Script foo has no content at scripts.foo.content + in databricks.yml:5:7 Exit code: 1 diff --git a/acceptance/bundle/summary/engine-terraform/output.txt b/acceptance/bundle/summary/engine-terraform/output.txt index 815ccf11750..5a56fcf634a 100644 --- a/acceptance/bundle/summary/engine-terraform/output.txt +++ b/acceptance/bundle/summary/engine-terraform/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle summary Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -19,6 +20,7 @@ Resources: >>> [CLI] bundle summary -o json Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -28,6 +30,7 @@ See https://docs.databricks.com/dev-tools/bundles/direct for details >>> musterr [CLI] bundle validate Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -40,6 +43,7 @@ Found 1 error >>> musterr [CLI] bundle validate -o json Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt index 32b813560a0..d494e84ba4c 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt @@ -44,6 +44,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:23:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/user_not_declared/files... @@ -107,6 +108,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:47:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/shared_users_can_manage/files... diff --git a/bundle/tests/sync_include_exclude_no_matches_test.go b/bundle/tests/sync_include_exclude_no_matches_test.go index aaea07bb236..5ca0ba1ee22 100644 --- a/bundle/tests/sync_include_exclude_no_matches_test.go +++ b/bundle/tests/sync_include_exclude_no_matches_test.go @@ -3,6 +3,7 @@ package config_tests import ( "fmt" "path" + "path/filepath" "testing" "github.com/databricks/cli/bundle" @@ -28,7 +29,7 @@ func TestSyncIncludeExcludeNoMatchesTest(t *testing.T) { // Locations are filled from Paths by logdiag when the diagnostic is logged. locations := b.Config.NearestLocations(diags[0].Paths[0]) assert.Len(t, locations, 1) - require.Equal(t, locations[0].File, path.Join("sync", "override", "databricks.yml")) + require.Equal(t, path.Join("sync", "override", "databricks.yml"), filepath.ToSlash(locations[0].File)) require.Equal(t, 17, locations[0].Line) require.Equal(t, 11, locations[0].Column) From 897e943f1afff3c6437efc0cddbe589c882e8ef3 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 9 Oct 2026 13:39:43 +0200 Subject: [PATCH 09/14] Fix codex review findings - compute_id warning keeps its location explicitly: the key is deleted before the diagnostic is logged. - Diagnostics about an effective value (invalid engine, trigger_pause_status, name_prefix) keep only its effective location; the automatic lookup would add the overridden ones. - validateJobTask no longer needs the bundle. Co-authored-by: Isaac --- bundle/config/mutator/compute_id_compat.go | 4 +++- bundle/config/mutator/compute_id_compate_test.go | 13 +++++++++++++ .../config/mutator/resourcemutator/apply_presets.go | 2 ++ .../mutator/resourcemutator/validate_target_mode.go | 4 ++++ bundle/config/validate/job_task_cluster_spec.go | 6 +++--- bundle/config/validate/validate_engine.go | 2 ++ 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/bundle/config/mutator/compute_id_compat.go b/bundle/config/mutator/compute_id_compat.go index 833b700228e..0455ae72b67 100644 --- a/bundle/config/mutator/compute_id_compat.go +++ b/bundle/config/mutator/compute_id_compat.go @@ -59,7 +59,9 @@ func rewriteComputeIdToClusterId(b *bundle.Bundle, p *structpath.PathNode, v str diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, Summary: "compute_id is deprecated, please use cluster_id instead", - Paths: []*structpath.PathNode{structpath.NewStringKey(p, "compute_id")}, + // Explicit: the key is deleted below, before the diagnostic is logged. + Locations: computeId.Locations(), + Paths: []*structpath.PathNode{structpath.NewStringKey(p, "compute_id")}, }) err := b.Config.Assign(structpath.NewStringKey(p, "cluster_id"), computeId) diff --git a/bundle/config/mutator/compute_id_compate_test.go b/bundle/config/mutator/compute_id_compate_test.go index 728bae43191..0add944b167 100644 --- a/bundle/config/mutator/compute_id_compate_test.go +++ b/bundle/config/mutator/compute_id_compate_test.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/libs/diag" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestComputeIdToClusterId(t *testing.T) { @@ -54,3 +55,15 @@ func TestComputeIdToClusterIdInTargetOverride(t *testing.T) { assert.Equal(t, "compute_id is deprecated, please use cluster_id instead", diags[0].Summary) assert.Equal(t, diag.Warning, diags[0].Severity) } + +func TestComputeIdToClusterIdKeepsLocation(t *testing.T) { + root, diags := config.LoadFromBytes("databricks.yml", []byte("bundle:\n name: x\n compute_id: compute-id\n")) + require.NoError(t, diags.Error()) + b := &bundle.Bundle{Config: *root} + + diags = bundle.Apply(t.Context(), b, mutator.ComputeIdToClusterId()) + require.NoError(t, diags.Error()) + require.Len(t, diags, 1) + // The key is deleted before the diagnostic is logged, so its location is kept explicitly. + assert.Equal(t, []diag.Location{{File: "databricks.yml", Line: 3, Column: 15}}, diags[0].Locations) +} diff --git a/bundle/config/mutator/resourcemutator/apply_presets.go b/bundle/config/mutator/resourcemutator/apply_presets.go index 29ef6841d15..15b92c1d4d4 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets.go +++ b/bundle/config/mutator/resourcemutator/apply_presets.go @@ -354,6 +354,8 @@ func validatePauseStatus(b *bundle.Bundle) diag.Diagnostics { Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED", Severity: diag.Error, Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, }} } diff --git a/bundle/config/mutator/resourcemutator/validate_target_mode.go b/bundle/config/mutator/resourcemutator/validate_target_mode.go index 110f4c9f9a2..d0882f478be 100644 --- a/bundle/config/mutator/resourcemutator/validate_target_mode.go +++ b/bundle/config/mutator/resourcemutator/validate_target_mode.go @@ -57,6 +57,8 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { Severity: diag.Error, Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, }) } @@ -79,6 +81,8 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { Severity: diag.Error, Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", Paths: structpath.NewPathSlice("presets", "name_prefix"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("presets.name_prefix")}, }) } return diags diff --git a/bundle/config/validate/job_task_cluster_spec.go b/bundle/config/validate/job_task_cluster_spec.go index fbb658374eb..9ecaea9c908 100644 --- a/bundle/config/validate/job_task_cluster_spec.go +++ b/bundle/config/validate/job_task_cluster_spec.go @@ -34,14 +34,14 @@ func (v *jobTaskClusterSpec) Apply(ctx context.Context, b *bundle.Bundle) diag.D for taskIndex, task := range job.Tasks { taskPath := structpath.NewPath(resourcePath, "tasks", taskIndex) - diags = diags.Extend(validateJobTask(b, task, taskPath)) + diags = diags.Extend(validateJobTask(task, taskPath)) } } return diags } -func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.PathNode) diag.Diagnostics { +func validateJobTask(task jobs.Task, taskPath *structpath.PathNode) diag.Diagnostics { diags := diag.Diagnostics{} var specified []string @@ -74,7 +74,7 @@ func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.Path if task.ForEachTask != nil { forEachTaskPath := structpath.NewPath(taskPath, "for_each_task", "task") - diags = diags.Extend(validateJobTask(b, task.ForEachTask.Task, forEachTaskPath)) + diags = diags.Extend(validateJobTask(task.ForEachTask.Task, forEachTaskPath)) } if isComputeTask(task) && len(specified) == 0 { diff --git a/bundle/config/validate/validate_engine.go b/bundle/config/validate/validate_engine.go index 20097d8ad8b..6ad48eae426 100644 --- a/bundle/config/validate/validate_engine.go +++ b/bundle/config/validate/validate_engine.go @@ -46,6 +46,8 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos Severity: diag.Error, Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), Paths: structpath.NewPathSlice("bundle", "engine"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("bundle.engine")}, }) } From d5193bbca6ae90efd790b96ae9dce57917fc55bc Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 12 Oct 2026 13:15:35 +0200 Subject: [PATCH 10/14] acc: at-line for the permissions recommendation in immutable_folder_permissions (#7001) Co-authored-by: Isaac --- .../bundle/validate/immutable_folder_permissions/output.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/acceptance/bundle/validate/immutable_folder_permissions/output.txt b/acceptance/bundle/validate/immutable_folder_permissions/output.txt index 73c193f0520..c0d14193553 100644 --- a/acceptance/bundle/validate/immutable_folder_permissions/output.txt +++ b/acceptance/bundle/validate/immutable_folder_permissions/output.txt @@ -10,6 +10,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:8:3 Name: my-bundle From 478c3dd6b25a7566b78c2d6f5ffd2520e82091b9 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 14 Oct 2026 16:47:28 +0200 Subject: [PATCH 11/14] Update override_effective_location golden for auto-filled locations Co-authored-by: Isaac --- .../bundle/validate/override_effective_location/output.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/acceptance/bundle/validate/override_effective_location/output.txt b/acceptance/bundle/validate/override_effective_location/output.txt index e3fa5193367..fddbb6c2ebb 100644 --- a/acceptance/bundle/validate/override_effective_location/output.txt +++ b/acceptance/bundle/validate/override_effective_location/output.txt @@ -2,6 +2,7 @@ === engine overridden in target >>> [CLI] bundle validate -t bad_engine Error: invalid value "invalid_value" for bundle.engine (expected "direct") + at bundle.engine in databricks.yml:12:15 Name: test-bundle @@ -14,6 +15,7 @@ Exit code: 1 === trigger_pause_status overridden in development target >>> [CLI] bundle validate -t dev_trigger Error: target with 'mode: development' cannot set trigger pause status to UNPAUSED by default + at presets.trigger_pause_status in databricks.yml:17:29 Name: test-bundle @@ -29,6 +31,7 @@ Exit code: 1 === name_prefix overridden in development target >>> [CLI] bundle validate -t dev_prefix Error: prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development' + at presets.name_prefix in databricks.yml:21:20 Name: test-bundle From de4ad7bf958ab33965d7235f3a90b28ebd398314 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 14 Oct 2026 18:18:07 +0200 Subject: [PATCH 12/14] Update run-no-commit golden for auto-filled locations Co-authored-by: Isaac --- acceptance/bundle/migrate/auto/run-no-commit/output.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/acceptance/bundle/migrate/auto/run-no-commit/output.txt b/acceptance/bundle/migrate/auto/run-no-commit/output.txt index 87ab33b2c30..1234350ab7c 100644 --- a/acceptance/bundle/migrate/auto/run-no-commit/output.txt +++ b/acceptance/bundle/migrate/auto/run-no-commit/output.txt @@ -24,6 +24,7 @@ ID Type Language Path >>> [CLI] bundle run test_job --no-wait Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. From 09764961d767d9326050fc1820270fcc37f6a425 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 15 Oct 2026 17:56:59 +0200 Subject: [PATCH 13/14] Use the Nearest location lookups in code restacked onto the renamed API Co-authored-by: Isaac --- bundle/config/mutator/resourcemutator/apply_presets.go | 2 +- bundle/config/mutator/resourcemutator/run_as_test.go | 1 - bundle/config/mutator/resourcemutator/validate_target_mode.go | 4 ++-- bundle/config/validate/validate_engine.go | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/apply_presets.go b/bundle/config/mutator/resourcemutator/apply_presets.go index 15b92c1d4d4..92db0182ec9 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets.go +++ b/bundle/config/mutator/resourcemutator/apply_presets.go @@ -355,7 +355,7 @@ func validatePauseStatus(b *bundle.Bundle) diag.Diagnostics { Severity: diag.Error, Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), // Only the effective location; the automatic lookup would also add overridden ones. - Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, + Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "trigger_pause_status"))}, }} } diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index da8c2d4941d..2946762e8a0 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -10,7 +10,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/databricks/databricks-sdk-go/service/pipelines" diff --git a/bundle/config/mutator/resourcemutator/validate_target_mode.go b/bundle/config/mutator/resourcemutator/validate_target_mode.go index d0882f478be..d085756a2f8 100644 --- a/bundle/config/mutator/resourcemutator/validate_target_mode.go +++ b/bundle/config/mutator/resourcemutator/validate_target_mode.go @@ -58,7 +58,7 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), // Only the effective location; the automatic lookup would also add overridden ones. - Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, + Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "trigger_pause_status"))}, }) } @@ -82,7 +82,7 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", Paths: structpath.NewPathSlice("presets", "name_prefix"), // Only the effective location; the automatic lookup would also add overridden ones. - Locations: []diag.Location{b.Config.GetLocation("presets.name_prefix")}, + Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "presets", "name_prefix"))}, }) } return diags diff --git a/bundle/config/validate/validate_engine.go b/bundle/config/validate/validate_engine.go index 6ad48eae426..72331a5570a 100644 --- a/bundle/config/validate/validate_engine.go +++ b/bundle/config/validate/validate_engine.go @@ -47,7 +47,7 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), Paths: structpath.NewPathSlice("bundle", "engine"), // Only the effective location; the automatic lookup would also add overridden ones. - Locations: []diag.Location{b.Config.GetLocation("bundle.engine")}, + Locations: []diag.Location{b.Config.NearestLocation(structpath.NewPath(nil, "bundle", "engine"))}, }) } From 9ec55269f515e985689791e264c2df90d7ec2041 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 15 Oct 2026 18:11:03 +0200 Subject: [PATCH 14/14] Restore raw-string diagnostic text re-indented during the restack Co-authored-by: Isaac --- .../validate/no_interpolation_in_bundle_name.go | 6 +++--- bundle/config/validate/scripts.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bundle/config/validate/no_interpolation_in_bundle_name.go b/bundle/config/validate/no_interpolation_in_bundle_name.go index 475906ec628..ef1ea23ed57 100644 --- a/bundle/config/validate/no_interpolation_in_bundle_name.go +++ b/bundle/config/validate/no_interpolation_in_bundle_name.go @@ -25,9 +25,9 @@ func (m *noInterpolationInBundleName) Apply(ctx context.Context, b *bundle.Bundl logdiag.LogDiag(ctx, diag.Diagnostic{ Severity: diag.Warning, Summary: `Please do not use variable interpolation in the name of your bundle. The name of your bundle - is a part of the path at which your bundle state is stored at by default. Parameterizing it at - runtime can have unexpected consequences like duplicate deployments or resources not being - cleaned up during bundle destroy.`, +is a part of the path at which your bundle state is stored at by default. Parameterizing it at +runtime can have unexpected consequences like duplicate deployments or resources not being +cleaned up during bundle destroy.`, Paths: structpath.NewPathSlice("bundle", "name"), }) } diff --git a/bundle/config/validate/scripts.go b/bundle/config/validate/scripts.go index 48b5ed0e7ee..73943afd289 100644 --- a/bundle/config/validate/scripts.go +++ b/bundle/config/validate/scripts.go @@ -70,14 +70,14 @@ func validateScriptContent(b *bundle.Bundle, key, content string, p *structpath. Severity: diag.Error, Summary: fmt.Sprintf("Found %s in script %s.content. Interpolation syntax ${...} is not supported in script content", first, key), Detail: `The ${...} syntax is not allowed in script content. To use a bundle value, - declare an environment variable in the script's "env:" section and reference it - from "content" with $NAME: - - scripts: - ` + key + `: - env: - MY_VAR: ${var.foo} - content: echo "$MY_VAR"`, +declare an environment variable in the script's "env:" section and reference it +from "content" with $NAME: + + scripts: + ` + key + `: + env: + MY_VAR: ${var.foo} + content: echo "$MY_VAR"`, Paths: []*structpath.PathNode{p}, }} }