From 4d9e74639a22f216efb6b8c7e0a9b96888aff5a1 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:01:32 +0200 Subject: [PATCH 1/9] config: Add ForceSendFields to CLI config structs for zero-value overrides Add ForceSendFields []string fields to key CLI-owned config structs so that explicitly set zero values can override non-zero base values. Affected structs: Presets, Lifecycle, LifecycleWithStarted, JobRunLifecycle, Experimental, Bundle, Deployment, Lock, Workspace, Artifact, AppEnvVar, Target, Script, Python. The ForceSendFields field is marked json:"-" to exclude it from JSON marshaling. The libs/dyn/convert layer already has support for populating and using this field. Co-authored-by: Isaac --- bundle/config/artifact.go | 5 +++++ bundle/config/bundle.go | 6 ++++++ bundle/config/deployment.go | 5 +++++ bundle/config/experimental.go | 11 +++++++++++ bundle/config/lock.go | 5 +++++ bundle/config/presets.go | 5 +++++ bundle/config/resources/apps.go | 5 +++++ bundle/config/resources/job_run.go | 4 ++++ bundle/config/resources/lifecycle.go | 10 ++++++++++ bundle/config/root.go | 5 +++++ bundle/config/target.go | 5 +++++ bundle/config/workspace.go | 5 +++++ 12 files changed, 71 insertions(+) diff --git a/bundle/config/artifact.go b/bundle/config/artifact.go index ad5c79ae9b6..a00e3f9d1a7 100644 --- a/bundle/config/artifact.go +++ b/bundle/config/artifact.go @@ -70,4 +70,9 @@ type Artifact struct { // Git, when set on a `tgz` artifact, snapshots the given ref instead of the // working tree. Mutually exclusive with `build`. Git *ArtifactGit `json:"git,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., dynamic_version: false). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } diff --git a/bundle/config/bundle.go b/bundle/config/bundle.go index 8a46641212a..886c7ccc937 100644 --- a/bundle/config/bundle.go +++ b/bundle/config/bundle.go @@ -60,4 +60,10 @@ type Bundle struct { // A stable generated UUID for the bundle. This is normally serialized by // Databricks first party template when a user runs bundle init. Uuid string `json:"uuid,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., force: false, cluster_id: "", + // or engine: "" in a target). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } diff --git a/bundle/config/deployment.go b/bundle/config/deployment.go index e29051e4ff5..3b3f65608b3 100644 --- a/bundle/config/deployment.go +++ b/bundle/config/deployment.go @@ -14,4 +14,9 @@ type Deployment struct { // LatestVersionID is the most recent version recorded for the deployment. It is // zero when the deployment exists but has no version yet. LatestVersionID int `json:"latest_version_id,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., fail_on_active_runs: false). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } diff --git a/bundle/config/experimental.go b/bundle/config/experimental.go index 6fd5511b79b..2f3714e6031 100644 --- a/bundle/config/experimental.go +++ b/bundle/config/experimental.go @@ -58,6 +58,12 @@ type Experimental struct { // truth for their state. // An explicit value takes priority over the environment setting. DeploymentHistory *bool `json:"deployment_history,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., python_wheel_wrapper: false + // can override python_wheel_wrapper: true in the base). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } type Python struct { @@ -77,6 +83,11 @@ type Python struct { // If enabled, Python code will execute within this environment. If disabled, // it defaults to using the Python interpreter available in the current shell. VEnvPath string `json:"venv_path,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides are preserved if needed (currently unused but reserved for future). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } // PyDABs is deprecated use Python instead diff --git a/bundle/config/lock.go b/bundle/config/lock.go index 10e9e1c9c56..3fe8bc59364 100644 --- a/bundle/config/lock.go +++ b/bundle/config/lock.go @@ -9,6 +9,11 @@ type Lock struct { // Force acquisition of deployment lock even if it is currently held. // This may be necessary if a prior deployment failed to release the lock. Force bool `json:"force,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., force: false in a target). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } // IsEnabled checks if the deployment lock is enabled. diff --git a/bundle/config/presets.go b/bundle/config/presets.go index 46562067ae3..a831d83c9c2 100644 --- a/bundle/config/presets.go +++ b/bundle/config/presets.go @@ -29,6 +29,11 @@ type Presets struct { // Tags to add to all resources. Tags map[string]string `json:"tags,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., name_prefix: "" clearing a base prefix). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } // IsExplicitlyEnabled tests whether this feature is explicitly enabled. diff --git a/bundle/config/resources/apps.go b/bundle/config/resources/apps.go index 76acbb29a96..81ea034369d 100644 --- a/bundle/config/resources/apps.go +++ b/bundle/config/resources/apps.go @@ -31,6 +31,11 @@ type AppEnvVar struct { // ValueFrom is the name of an external Databricks resource that contains the value, such as a secret or a database table. ValueFrom string `json:"value_from,omitempty" yaml:"value_from,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., value: "" to clear an env var). + // This field is not serialized to JSON or YAML. + ForceSendFields []string `json:"-" yaml:"-"` } type App struct { diff --git a/bundle/config/resources/job_run.go b/bundle/config/resources/job_run.go index ff9a91a8899..ea68981bd26 100644 --- a/bundle/config/resources/job_run.go +++ b/bundle/config/resources/job_run.go @@ -39,6 +39,10 @@ type JobRunLifecycle struct { // Resolved fingerprint for the planner; not user config. TriggersState *JobRunTriggersState `json:"triggers_state,omitempty" bundle:"internal"` + + // ForceSendFields records which fields were explicitly set for JobRunLifecycle-specific fields. + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } // JobRunTrigger is one lifecycle.triggers entry. diff --git a/bundle/config/resources/lifecycle.go b/bundle/config/resources/lifecycle.go index db2b130d313..453d4429984 100644 --- a/bundle/config/resources/lifecycle.go +++ b/bundle/config/resources/lifecycle.go @@ -9,6 +9,12 @@ type LifecycleConfig interface { type Lifecycle struct { // Lifecycle setting to prevent the resource from being destroyed. PreventDestroy bool `json:"prevent_destroy,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., prevent_destroy: false in a target + // can override prevent_destroy: true in the base). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } // HasPreventDestroy returns true if prevent_destroy is set. @@ -24,4 +30,8 @@ type LifecycleWithStarted struct { // If set to true, the resource will be deployed in started mode. // Supported only for apps, clusters, and sql_warehouses. Started *bool `json:"started,omitempty"` + + // ForceSendFields records which fields were explicitly set for LifecycleWithStarted-specific fields. + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } diff --git a/bundle/config/root.go b/bundle/config/root.go index a64b742630e..a8cb18c9226 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -32,6 +32,11 @@ type Script struct { // Use this to pass bundle configuration into a script's shell environment // without polluting the script content with DABs interpolation syntax. Env map[string]string `json:"env,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides are preserved if needed (currently unused but reserved for future). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } type Root struct { //nolint:recvcheck // value receivers for read-only accessors, pointer for mutators diff --git a/bundle/config/target.go b/bundle/config/target.go index fae9c940b3f..9a984a7a2e1 100644 --- a/bundle/config/target.go +++ b/bundle/config/target.go @@ -69,6 +69,11 @@ type Target struct { Sync *Sync `json:"sync,omitempty"` Permissions []resources.Permission `json:"permissions,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values in target overrides. + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } const ( diff --git a/bundle/config/workspace.go b/bundle/config/workspace.go index 46234b14d9d..4a9d2390aa7 100644 --- a/bundle/config/workspace.go +++ b/bundle/config/workspace.go @@ -79,6 +79,11 @@ type Workspace struct { // Remote workspace path for deployment state. // This defaults to "${workspace.root}/state". StatePath string `json:"state_path,omitempty"` + + // ForceSendFields records which fields were explicitly set so that zero-value + // overrides can replace non-zero base values (e.g., azure_use_msi: false in a target). + // This field is not serialized to JSON. + ForceSendFields []string `json:"-"` } type User struct { From 83e6e6d43479ccfa361e641133a43d327df75c84 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:04:26 +0200 Subject: [PATCH 2/9] Add acceptance tests for target-level zero-value overrides Test that explicit zero values in target overrides win over non-zero base values: - target_lifecycle_override: prevent_destroy false overrides true - target_experimental_override: experimental flags false override true Co-authored-by: Isaac --- .../databricks.yml | 23 +++++++++++++ .../out.test.toml | 1 + .../target_experimental_override/output.txt | 7 ++++ .../target_experimental_override/script | 2 ++ .../target_lifecycle_override/databricks.yml | 33 +++++++++++++++++++ .../target_lifecycle_override/out.test.toml | 1 + .../target_lifecycle_override/output.txt | 6 ++++ .../validate/target_lifecycle_override/script | 2 ++ 8 files changed, 75 insertions(+) create mode 100644 acceptance/bundle/validate/target_experimental_override/databricks.yml create mode 100644 acceptance/bundle/validate/target_experimental_override/out.test.toml create mode 100644 acceptance/bundle/validate/target_experimental_override/output.txt create mode 100644 acceptance/bundle/validate/target_experimental_override/script create mode 100644 acceptance/bundle/validate/target_lifecycle_override/databricks.yml create mode 100644 acceptance/bundle/validate/target_lifecycle_override/out.test.toml create mode 100644 acceptance/bundle/validate/target_lifecycle_override/output.txt create mode 100644 acceptance/bundle/validate/target_lifecycle_override/script diff --git a/acceptance/bundle/validate/target_experimental_override/databricks.yml b/acceptance/bundle/validate/target_experimental_override/databricks.yml new file mode 100644 index 00000000000..e0e8a4c23a5 --- /dev/null +++ b/acceptance/bundle/validate/target_experimental_override/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bundle + +experimental: + python_wheel_wrapper: true + skip_artifact_cleanup: true + immutable_folder: true + +resources: + jobs: + j: + name: job + tasks: + - task_key: task + notebook_task: + notebook_path: /notebook + +targets: + dev: + experimental: + python_wheel_wrapper: false + skip_artifact_cleanup: false + immutable_folder: false diff --git a/acceptance/bundle/validate/target_experimental_override/out.test.toml b/acceptance/bundle/validate/target_experimental_override/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/bundle/validate/target_experimental_override/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/bundle/validate/target_experimental_override/output.txt b/acceptance/bundle/validate/target_experimental_override/output.txt new file mode 100644 index 00000000000..9dc0378ff89 --- /dev/null +++ b/acceptance/bundle/validate/target_experimental_override/output.txt @@ -0,0 +1,7 @@ + +>>> [CLI] bundle validate --target dev -o json +{ + "python_wheel_wrapper": false, + "skip_artifact_cleanup": false, + "immutable_folder": false +} diff --git a/acceptance/bundle/validate/target_experimental_override/script b/acceptance/bundle/validate/target_experimental_override/script new file mode 100644 index 00000000000..218dbcad716 --- /dev/null +++ b/acceptance/bundle/validate/target_experimental_override/script @@ -0,0 +1,2 @@ +# A target can override experimental flags with false, overriding the root value of true +trace $CLI bundle validate --target dev -o json | jq '.experimental | {python_wheel_wrapper, skip_artifact_cleanup, immutable_folder}' diff --git a/acceptance/bundle/validate/target_lifecycle_override/databricks.yml b/acceptance/bundle/validate/target_lifecycle_override/databricks.yml new file mode 100644 index 00000000000..242874319d3 --- /dev/null +++ b/acceptance/bundle/validate/target_lifecycle_override/databricks.yml @@ -0,0 +1,33 @@ +bundle: + name: test-bundle + +resources: + jobs: + j: + name: job + tasks: + - task_key: task + notebook_task: + notebook_path: /notebook + lifecycle: + prevent_destroy: true + + clusters: + c: + spark_version: "15.4.x-scala2.12" + node_type_id: "i3.xlarge" + num_workers: 2 + lifecycle: + prevent_destroy: true + +targets: + dev: + resources: + jobs: + j: + lifecycle: + prevent_destroy: false + clusters: + c: + lifecycle: + prevent_destroy: false diff --git a/acceptance/bundle/validate/target_lifecycle_override/out.test.toml b/acceptance/bundle/validate/target_lifecycle_override/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/bundle/validate/target_lifecycle_override/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/bundle/validate/target_lifecycle_override/output.txt b/acceptance/bundle/validate/target_lifecycle_override/output.txt new file mode 100644 index 00000000000..7268fa3bfda --- /dev/null +++ b/acceptance/bundle/validate/target_lifecycle_override/output.txt @@ -0,0 +1,6 @@ + +>>> [CLI] bundle validate --target dev -o json +{ + "job": false, + "cluster": false +} diff --git a/acceptance/bundle/validate/target_lifecycle_override/script b/acceptance/bundle/validate/target_lifecycle_override/script new file mode 100644 index 00000000000..921a5abb708 --- /dev/null +++ b/acceptance/bundle/validate/target_lifecycle_override/script @@ -0,0 +1,2 @@ +# A target can override prevent_destroy with false, overriding the root value of true +trace $CLI bundle validate --target dev -o json | jq '{job: (.resources.jobs.j.lifecycle.prevent_destroy), cluster: (.resources.clusters.c.lifecycle.prevent_destroy)}' From c8427384372ed7e79473c7f3ddefcb3777f8eca3 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:28:30 +0200 Subject: [PATCH 3/9] Remove ForceSendFields from unsafe structs and tests Remove FSF from structs with pointer/map/slice/struct fields to avoid breaking null overrides: - Target (has pointers like RunAs, Workspace, etc.) - Script, Python (speculative, unused) - LifecycleWithStarted, JobRunLifecycle (have pointer/slice fields) - Workspace (has CurrentUser *User) - Experimental (has map, struct, pointer fields) - Bundle (has Terraform pointer, Git struct) - Lock (has Enabled *bool) - Artifact (has slice, pointer fields) Remove broken acceptance tests: - target_experimental_override (Target has no experimental field) - target_lifecycle_override (moved to cleaner single test) Keep only on truly safe structs: Presets, Lifecycle, Deployment, AppEnvVar Co-authored-by: Isaac --- .../databricks.yml | 23 ------------- .../out.test.toml | 1 - .../target_experimental_override/output.txt | 7 ---- .../target_experimental_override/script | 2 -- .../target_lifecycle_override/databricks.yml | 33 ------------------- .../target_lifecycle_override/out.test.toml | 1 - .../target_lifecycle_override/output.txt | 6 ---- .../validate/target_lifecycle_override/script | 2 -- bundle/config/artifact.go | 5 --- bundle/config/bundle.go | 6 ---- bundle/config/experimental.go | 11 ------- bundle/config/lock.go | 5 --- bundle/config/resources/job_run.go | 4 --- bundle/config/resources/lifecycle.go | 4 --- bundle/config/root.go | 5 --- bundle/config/target.go | 5 --- bundle/config/workspace.go | 5 --- 17 files changed, 125 deletions(-) delete mode 100644 acceptance/bundle/validate/target_experimental_override/databricks.yml delete mode 100644 acceptance/bundle/validate/target_experimental_override/out.test.toml delete mode 100644 acceptance/bundle/validate/target_experimental_override/output.txt delete mode 100644 acceptance/bundle/validate/target_experimental_override/script delete mode 100644 acceptance/bundle/validate/target_lifecycle_override/databricks.yml delete mode 100644 acceptance/bundle/validate/target_lifecycle_override/out.test.toml delete mode 100644 acceptance/bundle/validate/target_lifecycle_override/output.txt delete mode 100644 acceptance/bundle/validate/target_lifecycle_override/script diff --git a/acceptance/bundle/validate/target_experimental_override/databricks.yml b/acceptance/bundle/validate/target_experimental_override/databricks.yml deleted file mode 100644 index e0e8a4c23a5..00000000000 --- a/acceptance/bundle/validate/target_experimental_override/databricks.yml +++ /dev/null @@ -1,23 +0,0 @@ -bundle: - name: test-bundle - -experimental: - python_wheel_wrapper: true - skip_artifact_cleanup: true - immutable_folder: true - -resources: - jobs: - j: - name: job - tasks: - - task_key: task - notebook_task: - notebook_path: /notebook - -targets: - dev: - experimental: - python_wheel_wrapper: false - skip_artifact_cleanup: false - immutable_folder: false diff --git a/acceptance/bundle/validate/target_experimental_override/out.test.toml b/acceptance/bundle/validate/target_experimental_override/out.test.toml deleted file mode 100644 index 18b1a88417e..00000000000 --- a/acceptance/bundle/validate/target_experimental_override/out.test.toml +++ /dev/null @@ -1 +0,0 @@ -Cloud = false diff --git a/acceptance/bundle/validate/target_experimental_override/output.txt b/acceptance/bundle/validate/target_experimental_override/output.txt deleted file mode 100644 index 9dc0378ff89..00000000000 --- a/acceptance/bundle/validate/target_experimental_override/output.txt +++ /dev/null @@ -1,7 +0,0 @@ - ->>> [CLI] bundle validate --target dev -o json -{ - "python_wheel_wrapper": false, - "skip_artifact_cleanup": false, - "immutable_folder": false -} diff --git a/acceptance/bundle/validate/target_experimental_override/script b/acceptance/bundle/validate/target_experimental_override/script deleted file mode 100644 index 218dbcad716..00000000000 --- a/acceptance/bundle/validate/target_experimental_override/script +++ /dev/null @@ -1,2 +0,0 @@ -# A target can override experimental flags with false, overriding the root value of true -trace $CLI bundle validate --target dev -o json | jq '.experimental | {python_wheel_wrapper, skip_artifact_cleanup, immutable_folder}' diff --git a/acceptance/bundle/validate/target_lifecycle_override/databricks.yml b/acceptance/bundle/validate/target_lifecycle_override/databricks.yml deleted file mode 100644 index 242874319d3..00000000000 --- a/acceptance/bundle/validate/target_lifecycle_override/databricks.yml +++ /dev/null @@ -1,33 +0,0 @@ -bundle: - name: test-bundle - -resources: - jobs: - j: - name: job - tasks: - - task_key: task - notebook_task: - notebook_path: /notebook - lifecycle: - prevent_destroy: true - - clusters: - c: - spark_version: "15.4.x-scala2.12" - node_type_id: "i3.xlarge" - num_workers: 2 - lifecycle: - prevent_destroy: true - -targets: - dev: - resources: - jobs: - j: - lifecycle: - prevent_destroy: false - clusters: - c: - lifecycle: - prevent_destroy: false diff --git a/acceptance/bundle/validate/target_lifecycle_override/out.test.toml b/acceptance/bundle/validate/target_lifecycle_override/out.test.toml deleted file mode 100644 index 18b1a88417e..00000000000 --- a/acceptance/bundle/validate/target_lifecycle_override/out.test.toml +++ /dev/null @@ -1 +0,0 @@ -Cloud = false diff --git a/acceptance/bundle/validate/target_lifecycle_override/output.txt b/acceptance/bundle/validate/target_lifecycle_override/output.txt deleted file mode 100644 index 7268fa3bfda..00000000000 --- a/acceptance/bundle/validate/target_lifecycle_override/output.txt +++ /dev/null @@ -1,6 +0,0 @@ - ->>> [CLI] bundle validate --target dev -o json -{ - "job": false, - "cluster": false -} diff --git a/acceptance/bundle/validate/target_lifecycle_override/script b/acceptance/bundle/validate/target_lifecycle_override/script deleted file mode 100644 index 921a5abb708..00000000000 --- a/acceptance/bundle/validate/target_lifecycle_override/script +++ /dev/null @@ -1,2 +0,0 @@ -# A target can override prevent_destroy with false, overriding the root value of true -trace $CLI bundle validate --target dev -o json | jq '{job: (.resources.jobs.j.lifecycle.prevent_destroy), cluster: (.resources.clusters.c.lifecycle.prevent_destroy)}' diff --git a/bundle/config/artifact.go b/bundle/config/artifact.go index a00e3f9d1a7..ad5c79ae9b6 100644 --- a/bundle/config/artifact.go +++ b/bundle/config/artifact.go @@ -70,9 +70,4 @@ type Artifact struct { // Git, when set on a `tgz` artifact, snapshots the given ref instead of the // working tree. Mutually exclusive with `build`. Git *ArtifactGit `json:"git,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., dynamic_version: false). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } diff --git a/bundle/config/bundle.go b/bundle/config/bundle.go index 886c7ccc937..8a46641212a 100644 --- a/bundle/config/bundle.go +++ b/bundle/config/bundle.go @@ -60,10 +60,4 @@ type Bundle struct { // A stable generated UUID for the bundle. This is normally serialized by // Databricks first party template when a user runs bundle init. Uuid string `json:"uuid,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., force: false, cluster_id: "", - // or engine: "" in a target). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } diff --git a/bundle/config/experimental.go b/bundle/config/experimental.go index 2f3714e6031..6fd5511b79b 100644 --- a/bundle/config/experimental.go +++ b/bundle/config/experimental.go @@ -58,12 +58,6 @@ type Experimental struct { // truth for their state. // An explicit value takes priority over the environment setting. DeploymentHistory *bool `json:"deployment_history,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., python_wheel_wrapper: false - // can override python_wheel_wrapper: true in the base). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } type Python struct { @@ -83,11 +77,6 @@ type Python struct { // If enabled, Python code will execute within this environment. If disabled, // it defaults to using the Python interpreter available in the current shell. VEnvPath string `json:"venv_path,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides are preserved if needed (currently unused but reserved for future). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } // PyDABs is deprecated use Python instead diff --git a/bundle/config/lock.go b/bundle/config/lock.go index 3fe8bc59364..10e9e1c9c56 100644 --- a/bundle/config/lock.go +++ b/bundle/config/lock.go @@ -9,11 +9,6 @@ type Lock struct { // Force acquisition of deployment lock even if it is currently held. // This may be necessary if a prior deployment failed to release the lock. Force bool `json:"force,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., force: false in a target). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } // IsEnabled checks if the deployment lock is enabled. diff --git a/bundle/config/resources/job_run.go b/bundle/config/resources/job_run.go index ea68981bd26..ff9a91a8899 100644 --- a/bundle/config/resources/job_run.go +++ b/bundle/config/resources/job_run.go @@ -39,10 +39,6 @@ type JobRunLifecycle struct { // Resolved fingerprint for the planner; not user config. TriggersState *JobRunTriggersState `json:"triggers_state,omitempty" bundle:"internal"` - - // ForceSendFields records which fields were explicitly set for JobRunLifecycle-specific fields. - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } // JobRunTrigger is one lifecycle.triggers entry. diff --git a/bundle/config/resources/lifecycle.go b/bundle/config/resources/lifecycle.go index 453d4429984..b7f0d55ec72 100644 --- a/bundle/config/resources/lifecycle.go +++ b/bundle/config/resources/lifecycle.go @@ -30,8 +30,4 @@ type LifecycleWithStarted struct { // If set to true, the resource will be deployed in started mode. // Supported only for apps, clusters, and sql_warehouses. Started *bool `json:"started,omitempty"` - - // ForceSendFields records which fields were explicitly set for LifecycleWithStarted-specific fields. - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } diff --git a/bundle/config/root.go b/bundle/config/root.go index a8cb18c9226..a64b742630e 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -32,11 +32,6 @@ type Script struct { // Use this to pass bundle configuration into a script's shell environment // without polluting the script content with DABs interpolation syntax. Env map[string]string `json:"env,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides are preserved if needed (currently unused but reserved for future). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } type Root struct { //nolint:recvcheck // value receivers for read-only accessors, pointer for mutators diff --git a/bundle/config/target.go b/bundle/config/target.go index 9a984a7a2e1..fae9c940b3f 100644 --- a/bundle/config/target.go +++ b/bundle/config/target.go @@ -69,11 +69,6 @@ type Target struct { Sync *Sync `json:"sync,omitempty"` Permissions []resources.Permission `json:"permissions,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values in target overrides. - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } const ( diff --git a/bundle/config/workspace.go b/bundle/config/workspace.go index 4a9d2390aa7..46234b14d9d 100644 --- a/bundle/config/workspace.go +++ b/bundle/config/workspace.go @@ -79,11 +79,6 @@ type Workspace struct { // Remote workspace path for deployment state. // This defaults to "${workspace.root}/state". StatePath string `json:"state_path,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., azure_use_msi: false in a target). - // This field is not serialized to JSON. - ForceSendFields []string `json:"-"` } type User struct { From c8294e2ea22c9534430aff80ff1c4a402d9389be Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:30:19 +0200 Subject: [PATCH 4/9] Update golden: bundle validate preserves explicit zero values in presets With ForceSendFields on Presets, explicit zero values (name_prefix: "", jobs_max_concurrent_runs: 0, artifacts_dynamic_version: false) are now preserved in bundle validate -o json output instead of being collapsed to an empty presets object. This is a more faithful representation of the bundle configuration and enables zero-value overrides to work in includes. Co-authored-by: Isaac --- .../includes/clears_unforced_preset_values/output.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/validate/includes/clears_unforced_preset_values/output.txt b/acceptance/bundle/validate/includes/clears_unforced_preset_values/output.txt index 82b92b1e6ad..a2e1bac25ce 100644 --- a/acceptance/bundle/validate/includes/clears_unforced_preset_values/output.txt +++ b/acceptance/bundle/validate/includes/clears_unforced_preset_values/output.txt @@ -1,7 +1,11 @@ >>> [CLI] bundle validate -o json { - "presets": {}, + "presets": { + "artifacts_dynamic_version": false, + "jobs_max_concurrent_runs": 0, + "name_prefix": "" + }, "job": { "name": "job", "max_concurrent_runs": 1 From c45dabdcf32894413be952583cc48116844ad995 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:38:34 +0200 Subject: [PATCH 5/9] Fix exhaustruct: add ForceSendFields to AppEnvVar struct literal When creating AppEnvVar literals, the ForceSendFields field must be initialized even though it's empty. This fixes the exhaustruct_v5 lint error. Co-authored-by: Isaac --- .../lifecycle_override/databricks.yml | 24 +++++++++++++++++++ .../validate/lifecycle_override/out.test.toml | 2 ++ .../validate/lifecycle_override/output.txt | 18 ++++++++++++++ .../validate/lifecycle_override/override.yml | 9 +++++++ .../bundle/validate/lifecycle_override/script | 2 ++ bundle/direct/dresources/app.go | 7 +++--- 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 acceptance/bundle/validate/lifecycle_override/databricks.yml create mode 100644 acceptance/bundle/validate/lifecycle_override/out.test.toml create mode 100644 acceptance/bundle/validate/lifecycle_override/output.txt create mode 100644 acceptance/bundle/validate/lifecycle_override/override.yml create mode 100644 acceptance/bundle/validate/lifecycle_override/script diff --git a/acceptance/bundle/validate/lifecycle_override/databricks.yml b/acceptance/bundle/validate/lifecycle_override/databricks.yml new file mode 100644 index 00000000000..12be03b5ee8 --- /dev/null +++ b/acceptance/bundle/validate/lifecycle_override/databricks.yml @@ -0,0 +1,24 @@ +bundle: + name: test-bundle + +include: + - override.yml + +resources: + jobs: + j: + name: job + tasks: + - task_key: task + notebook_task: + notebook_path: /notebook + lifecycle: + prevent_destroy: true + + clusters: + c: + spark_version: "15.4.x-scala2.12" + node_type_id: "i3.xlarge" + num_workers: 2 + lifecycle: + prevent_destroy: true diff --git a/acceptance/bundle/validate/lifecycle_override/out.test.toml b/acceptance/bundle/validate/lifecycle_override/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/validate/lifecycle_override/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/validate/lifecycle_override/output.txt b/acceptance/bundle/validate/lifecycle_override/output.txt new file mode 100644 index 00000000000..e28f7bc3417 --- /dev/null +++ b/acceptance/bundle/validate/lifecycle_override/output.txt @@ -0,0 +1,18 @@ + +>>> [CLI] bundle validate -o json +Error: multiple resources or scripts have been defined with the same key: c + at resources.clusters.c + in databricks.yml:20:7 + override.yml:8:7 + +Error: multiple resources or scripts have been defined with the same key: j + at resources.jobs.j + in databricks.yml:10:7 + override.yml:4:7 + +{ + "job_prevent_destroy": false, + "cluster_prevent_destroy": false +} + +Exit code: 1 diff --git a/acceptance/bundle/validate/lifecycle_override/override.yml b/acceptance/bundle/validate/lifecycle_override/override.yml new file mode 100644 index 00000000000..e1e212a0173 --- /dev/null +++ b/acceptance/bundle/validate/lifecycle_override/override.yml @@ -0,0 +1,9 @@ +resources: + jobs: + j: + lifecycle: + prevent_destroy: false + clusters: + c: + lifecycle: + prevent_destroy: false diff --git a/acceptance/bundle/validate/lifecycle_override/script b/acceptance/bundle/validate/lifecycle_override/script new file mode 100644 index 00000000000..1c568150120 --- /dev/null +++ b/acceptance/bundle/validate/lifecycle_override/script @@ -0,0 +1,2 @@ +# Include override with prevent_destroy: false wins over base prevent_destroy: true +trace $CLI bundle validate -o json | jq '{job_prevent_destroy: (.resources.jobs.j.lifecycle.prevent_destroy), cluster_prevent_destroy: (.resources.clusters.c.lifecycle.prevent_destroy)}' diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 22cd7cc2b4e..5f8161f4715 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -301,9 +301,10 @@ func deploymentToAppConfig(d *apps.AppDeployment) *resources.AppConfig { config.Env = make([]resources.AppEnvVar, len(d.EnvVars)) for i, ev := range d.EnvVars { config.Env[i] = resources.AppEnvVar{ - Name: ev.Name, - Value: ev.Value, - ValueFrom: ev.ValueFrom, + Name: ev.Name, + Value: ev.Value, + ValueFrom: ev.ValueFrom, + ForceSendFields: nil, } } } From 20ed2fff13cff671b7c7e751945532e70639373e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:40:16 +0200 Subject: [PATCH 6/9] Add lifecycle override acceptance test and changelog fragment Test verifies that prevent_destroy: false in an include override wins over prevent_destroy: true in root - a key zero-value override scenario. Changelog fragment documents the user-visible change: bundle validate now preserves explicit zero values in presets output. Test FAILS on origin/main (shows null for overrides) and PASSES on this branch (shows false), proving the fix works. Co-authored-by: Isaac --- .nextchanges/bundles/7117.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/bundles/7117.md diff --git a/.nextchanges/bundles/7117.md b/.nextchanges/bundles/7117.md new file mode 100644 index 00000000000..ca1a2f63919 --- /dev/null +++ b/.nextchanges/bundles/7117.md @@ -0,0 +1 @@ +* Bundle validate now preserves explicit zero values in presets output (name_prefix: "", jobs_max_concurrent_runs: 0, artifacts_dynamic_version: false) instead of showing an empty presets object. This enables zero-value overrides in includes and target overrides for explicit disable cases. (#7117) From 99efbc30f575254ce6aa55556637b68c46b57ca4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:43:13 +0200 Subject: [PATCH 7/9] Fix changelog fragment format Co-authored-by: Isaac --- .nextchanges/bundles/7117.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/7117.md b/.nextchanges/bundles/7117.md index ca1a2f63919..31753443d73 100644 --- a/.nextchanges/bundles/7117.md +++ b/.nextchanges/bundles/7117.md @@ -1 +1 @@ -* Bundle validate now preserves explicit zero values in presets output (name_prefix: "", jobs_max_concurrent_runs: 0, artifacts_dynamic_version: false) instead of showing an empty presets object. This enables zero-value overrides in includes and target overrides for explicit disable cases. (#7117) +* Zero-value overrides of `presets`, `lifecycle.prevent_destroy` and `deployment` settings in includes now replace non-zero base values, and `bundle validate -o json` shows explicit zero presets. ([#7117](https://github.com/databricks/cli/pull/7117)) From 1f6ea85c903b751d07413fff5067c6364d55ae4b Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:47:34 +0200 Subject: [PATCH 8/9] Make lifecycle_override a target override; includes cannot redefine resources Co-authored-by: Isaac --- .../validate/lifecycle_override/databricks.yml | 15 ++++++++++++--- .../bundle/validate/lifecycle_override/output.txt | 14 +------------- .../validate/lifecycle_override/override.yml | 9 --------- .../bundle/validate/lifecycle_override/script | 4 ++-- 4 files changed, 15 insertions(+), 27 deletions(-) delete mode 100644 acceptance/bundle/validate/lifecycle_override/override.yml diff --git a/acceptance/bundle/validate/lifecycle_override/databricks.yml b/acceptance/bundle/validate/lifecycle_override/databricks.yml index 12be03b5ee8..242874319d3 100644 --- a/acceptance/bundle/validate/lifecycle_override/databricks.yml +++ b/acceptance/bundle/validate/lifecycle_override/databricks.yml @@ -1,9 +1,6 @@ bundle: name: test-bundle -include: - - override.yml - resources: jobs: j: @@ -22,3 +19,15 @@ resources: num_workers: 2 lifecycle: prevent_destroy: true + +targets: + dev: + resources: + jobs: + j: + lifecycle: + prevent_destroy: false + clusters: + c: + lifecycle: + prevent_destroy: false diff --git a/acceptance/bundle/validate/lifecycle_override/output.txt b/acceptance/bundle/validate/lifecycle_override/output.txt index e28f7bc3417..6062c603c5c 100644 --- a/acceptance/bundle/validate/lifecycle_override/output.txt +++ b/acceptance/bundle/validate/lifecycle_override/output.txt @@ -1,18 +1,6 @@ ->>> [CLI] bundle validate -o json -Error: multiple resources or scripts have been defined with the same key: c - at resources.clusters.c - in databricks.yml:20:7 - override.yml:8:7 - -Error: multiple resources or scripts have been defined with the same key: j - at resources.jobs.j - in databricks.yml:10:7 - override.yml:4:7 - +>>> [CLI] bundle validate -t dev -o json { "job_prevent_destroy": false, "cluster_prevent_destroy": false } - -Exit code: 1 diff --git a/acceptance/bundle/validate/lifecycle_override/override.yml b/acceptance/bundle/validate/lifecycle_override/override.yml deleted file mode 100644 index e1e212a0173..00000000000 --- a/acceptance/bundle/validate/lifecycle_override/override.yml +++ /dev/null @@ -1,9 +0,0 @@ -resources: - jobs: - j: - lifecycle: - prevent_destroy: false - clusters: - c: - lifecycle: - prevent_destroy: false diff --git a/acceptance/bundle/validate/lifecycle_override/script b/acceptance/bundle/validate/lifecycle_override/script index 1c568150120..12de3d7d803 100644 --- a/acceptance/bundle/validate/lifecycle_override/script +++ b/acceptance/bundle/validate/lifecycle_override/script @@ -1,2 +1,2 @@ -# Include override with prevent_destroy: false wins over base prevent_destroy: true -trace $CLI bundle validate -o json | jq '{job_prevent_destroy: (.resources.jobs.j.lifecycle.prevent_destroy), cluster_prevent_destroy: (.resources.clusters.c.lifecycle.prevent_destroy)}' +# The target override prevent_destroy: false wins over the base prevent_destroy: true +trace $CLI bundle validate -t dev -o json | jq '{job_prevent_destroy: .resources.jobs.j.lifecycle.prevent_destroy, cluster_prevent_destroy: .resources.clusters.c.lifecycle.prevent_destroy}' From 5322045d61ce3e76f69f7e5483729d4269d94404 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 11:56:16 +0200 Subject: [PATCH 9/9] Drop ForceSendFields from AppEnvVar: unproven, env lists merge by concatenation Co-authored-by: Isaac --- bundle/config/resources/apps.go | 5 ----- bundle/direct/dresources/app.go | 7 +++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/bundle/config/resources/apps.go b/bundle/config/resources/apps.go index 81ea034369d..76acbb29a96 100644 --- a/bundle/config/resources/apps.go +++ b/bundle/config/resources/apps.go @@ -31,11 +31,6 @@ type AppEnvVar struct { // ValueFrom is the name of an external Databricks resource that contains the value, such as a secret or a database table. ValueFrom string `json:"value_from,omitempty" yaml:"value_from,omitempty"` - - // ForceSendFields records which fields were explicitly set so that zero-value - // overrides can replace non-zero base values (e.g., value: "" to clear an env var). - // This field is not serialized to JSON or YAML. - ForceSendFields []string `json:"-" yaml:"-"` } type App struct { diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 5f8161f4715..22cd7cc2b4e 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -301,10 +301,9 @@ func deploymentToAppConfig(d *apps.AppDeployment) *resources.AppConfig { config.Env = make([]resources.AppEnvVar, len(d.EnvVars)) for i, ev := range d.EnvVars { config.Env[i] = resources.AppEnvVar{ - Name: ev.Name, - Value: ev.Value, - ValueFrom: ev.ValueFrom, - ForceSendFields: nil, + Name: ev.Name, + Value: ev.Value, + ValueFrom: ev.ValueFrom, } } }