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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/bundles/app-config-env-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fix `bundle plan` reporting an update to an app's `config.env` values on every run, which also redeployed the app on every `bundle deploy`. The Apps API returns environment variable names only, so `value` and `value_from` are no longer compared against the remote deployment. ([#7326](https://github.com/databricks/cli/pull/7326))
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged
"config.env[0].value": {
"action": "update",
"old": "original_value",
"new": "changed_value",
"remote": "original_value"
"new": "changed_value"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@
],
"remote": [
{
"name": "MY_VAR",
"value": "changed_value"
"name": "MY_VAR"
},
{
"name": "NEW_VAR",
"value": "new_value"
"name": "NEW_VAR"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import http.server, os

http.server.HTTPServer(
("", int(os.environ["DATABRICKS_APP_PORT"])), http.server.SimpleHTTPRequestHandler
).serve_forever()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
bundle:
name: apps-config-env-no-drift

resources:
apps:
myapp:
name: myapp
source_code_path: ./app
resources:
- name: my-secret
secret:
scope: my-scope
key: my-key
permission: READ
config:
command:
- python
- app.py
env:
- name: GREETING
value: hello
- name: API_TOKEN
value_from: my-secret
lifecycle:
started: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = false
EnvMatrix.DMS = ["", "true"]
81 changes: 81 additions & 0 deletions acceptance/bundle/resources/apps/config-env-no-drift/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@

=== Deploy a started app with config.env entries set via value and value_from
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default/files...
✓ Deployment succeeded
Created apps.myapp
Files: 2 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged

=== The Apps API returns deployment env vars with their name only
>>> [CLI] bundle plan -o json
[
{
"name": "GREETING"
},
{
"name": "API_TOKEN"
}
]

=== Re-plan is clean: value and value_from are input-only and must not drift
>>> [CLI] bundle plan
Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged

>>> [CLI] bundle plan -o json
{
"config.env[0].value": {
"action": "skip",
"reason": "input_only",
"old": "hello",
"new": "hello"
},
"config.env[1].value_from": {
"action": "skip",
"reason": "input_only",
"old": "my-secret",
"new": "my-secret"
}
}

=== A local change to an env value still plans an update
>>> update_file.py databricks.yml value: hello value: goodbye

>>> [CLI] bundle plan
update apps.myapp

Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged

>>> [CLI] bundle plan -o json
{
"config.env[0].value": {
"action": "update",
"old": "hello",
"new": "goodbye"
},
"config.env[1].value_from": {
"action": "skip",
"reason": "input_only",
"old": "my-secret",
"new": "my-secret"
}
}

=== Deploy applies the change and the plan is clean again
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default/files...
✓ Deployment succeeded
Updated apps.myapp
Files: 1 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged

>>> [CLI] bundle plan
Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.apps.myapp

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/apps-config-env-no-drift/default

Destroy: 1 deleted
23 changes: 23 additions & 0 deletions acceptance/bundle/resources/apps/config-env-no-drift/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
cleanup() {
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

title "Deploy a started app with config.env entries set via value and value_from"
trace $CLI bundle deploy

title "The Apps API returns deployment env vars with their name only"
trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".remote_state.config.env'

title "Re-plan is clean: value and value_from are input-only and must not drift"
trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged"
trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".changes // {} | with_entries(select(.key | startswith("config.env")))'

title "A local change to an env value still plans an update"
trace update_file.py databricks.yml "value: hello" "value: goodbye"
trace $CLI bundle plan | contains.py "Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged"
trace $CLI bundle plan -o json | nostamp | jq '.plan."resources.apps.myapp".changes // {} | with_entries(select(.key | startswith("config.env")))'

title "Deploy applies the change and the plan is clean again"
trace $CLI bundle deploy
trace $CLI bundle plan | contains.py "Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The Apps API returns deployment env vars with their name only, so config.env value and
# value_from are classified input-only (ignore_remote_changes) and the fake server models
# that masking. A real workspace would need the referenced secret to exist, so this runs
# locally only. See https://github.com/databricks/cli/issues/7301
RecordRequests = false
7 changes: 7 additions & 0 deletions bundle/direct/dresources/configs/apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ backend_defaults:
ignore_remote_changes:
- field: space # This field is not yet supported by Update APIs but exposed in the API spec. TODO: fix when update APIs supports it.
reason: managed
# The Apps API returns deployment env vars with their name only: value and value_from are

@janniklasrose janniklasrose Nov 3, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we fix this upstream in protos? (i.e. mark as input_only)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can, this is to fix it until addressed upstream

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new?

# never read back, so the remote side is always empty and would otherwise plan an update
# on every run. See https://github.com/databricks/cli/issues/7301
- field: config.env[*].value
reason: input_only
- field: config.env[*].value_from
reason: input_only
21 changes: 8 additions & 13 deletions libs/testserver/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ func (s *FakeWorkspace) AppsCreateDeployment(req Request, name string) Response
Message: "Deployment succeeded",
}

// The backend returns env vars with their name only: value and value_from are never
// read back, neither from get-deployment nor from the app's active_deployment. Mask
// them before storing so every read path agrees. See https://github.com/databricks/cli/issues/7301
for i, ev := range deployment.EnvVars {
deployment.EnvVars[i] = apps.EnvVar{Name: ev.Name}
}

app.ActiveDeployment = &deployment
app.DefaultSourceCodePath = deployment.SourceCodePath
s.Apps[name] = app
Expand All @@ -166,19 +173,7 @@ func (s *FakeWorkspace) AppsGetDeployment(_ Request, name, deploymentID string)
return Response{StatusCode: 404}
}

// Return a copy so the masking below does not mutate the stored deployment.
deployment := *app.ActiveDeployment

// The real GET response strips env var values, returning only the name of
// each variable. Reproduce that masking so the local golden matches
// recorded cloud behavior.
maskedEnvVars := make([]apps.EnvVar, len(deployment.EnvVars))
for i, ev := range deployment.EnvVars {
maskedEnvVars[i] = apps.EnvVar{Name: ev.Name}
}
deployment.EnvVars = maskedEnvVars

return Response{Body: deployment}
return Response{Body: app.ActiveDeployment}
}

func (s *FakeWorkspace) AppsStart(_ Request, name string) Response {
Expand Down
Loading