Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9bd0e2b
Add hidden --local flag to bundle plan and deploy
denik Jun 22, 2026
5dc4c25
direct: surface on-demand remote reads in --local plan
denik Jun 22, 2026
1b45494
acc: also run selected resource plan/deploy with --local
denik Jun 23, 2026
59dac16
direct: record local_only in plan JSON; warn on deploy --plan
denik Jun 24, 2026
b88c221
direct: simplify --local plumbing
denik Jul 2, 2026
8946ab8
acc: also print requests after bundle plan/deploy --local -o json
denik Jul 6, 2026
1a71619
# Regression Test Report
denik Jul 6, 2026
3c1c6c3
acc: add --local matrix to catalogs, external_locations, schemas/drif…
denik Jul 6, 2026
fb8ffbb
acc: test deploy --plan warning for --local plans; add READPLAN matrix
denik Jul 6, 2026
85bf9d6
acc: add local/references test for on-demand remote read of remote-on…
denik Jul 6, 2026
07fe699
acc: assert --local plan ignores remote drift on both tests
denik Jul 6, 2026
517c113
direct: use saved state as remote in --local plan
denik Jul 7, 2026
410f5db
acc: remove noisy Destroy title from cleanup
denik Jul 7, 2026
ff2cfaf
direct: fix nil-deref in ResourceApp.OverrideChangeDesc
denik Jul 8, 2026
6739954
direct: table-drive TestAppOverrideChangeDescSourceCodePath
denik Jul 8, 2026
fb15598
Rename --local to --plan-mode=local
denik Jul 13, 2026
de1959b
direct: don't put StateType in entry.RemoteState; reject offline mode
denik Jul 13, 2026
0d0f591
direct: split PlanEntry state into PriorState (StateType) and RemoteS…
denik Jul 21, 2026
db23f8a
direct: don't use PriorState fallback in full mode when remote is mis…
denik Jul 21, 2026
33ee8db
acc: regenerate local/rejected output to match reverted offline rejec…
denik Jul 21, 2026
b856d28
direct: only populate PriorState in local mode
denik Jul 21, 2026
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 acceptance/bundle/invariant/no_drift/out.test.toml

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

8 changes: 5 additions & 3 deletions acceptance/bundle/invariant/no_drift/script
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ cat LOG.deploy | contains.py '!panic' '!internal error' > /dev/null
# Any failures after this point will be considered as "bug detected" by fuzzer.
echo INPUT_CONFIG_OK

# Check both text and JSON plan for no changes
# Check both text and JSON plan for no changes.
# LOCAL_DIFF is either empty or "--plan-mode=local"; the local mode ignores the remote
# state of resources, so no drift means the local state saved by deploy matches config.
# Note, expect that there maybe more than one resource unchanged
$CLI bundle plan -o json > LOG.planjson 2>LOG.planjson.err
$CLI bundle plan $LOCAL_DIFF -o json > LOG.planjson 2>LOG.planjson.err
cat LOG.planjson.err | contains.py '!panic' '!internal error' > /dev/null
verify_no_drift.py LOG.planjson

$CLI bundle plan 2>LOG.plan.err | contains.py '!panic' '!internal error' 'Plan: 0 to add, 0 to change, 0 to delete' > LOG.plan
$CLI bundle plan $LOCAL_DIFF 2>LOG.plan.err | contains.py '!panic' '!internal error' 'Plan: 0 to add, 0 to change, 0 to delete' > LOG.plan
cat LOG.plan.err | contains.py '!panic' '!internal error' > /dev/null
5 changes: 5 additions & 0 deletions acceptance/bundle/invariant/no_drift/test.toml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
EnvMatrix.READPLAN = ["", "1"]

# Run every config twice: once planning normally and once with --local, which plans
# using only the local state. The no-drift invariant must hold either way: after a
# deploy the local state saved by the engine must already match the config.
EnvMatrix.LOCAL_DIFF = ["", "--plan-mode=local"]
14 changes: 14 additions & 0 deletions acceptance/bundle/local/basic/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bundle:
name: local-$UNIQUE_NAME

resources:
jobs:
bar:
name: bar-$UNIQUE_NAME

foo:
name: foo-$UNIQUE_NAME
tasks:
- task_key: run_bar
run_job_task:
job_id: ${resources.jobs.bar.id}
4 changes: 4 additions & 0 deletions acceptance/bundle/local/basic/out.test.toml

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

117 changes: 117 additions & 0 deletions acceptance/bundle/local/basic/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/local-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] bundle plan --plan-mode=local
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged

=== Job reads during 'bundle plan --plan-mode=local':

>>> print_requests.py --get //jobs/get

=== After out-of-band rename of bar: --plan-mode=local plan still shows no drift:

>>> [CLI] bundle plan --plan-mode=local
Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged

=== Job reads during --plan-mode=local plan after drift:

>>> print_requests.py --get //jobs/get

=== Plan --plan-mode=local as JSON:

>>> [CLI] bundle plan --plan-mode=local -o json
{
"plan_version": 2,
"cli_version": "[CLI_VERSION]",
"lineage": "[UUID]",
"serial": 1,
"plan_mode": "local",
"plan": {
"resources.jobs.bar": {
"action": "skip",
"prior_state": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/local-[UNIQUE_NAME]/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "bar-[UNIQUE_NAME]",
"queue": {
"enabled": true
}
}
},
"resources.jobs.foo": {
"depends_on": [
{
"node": "resources.jobs.bar",
"label": "${resources.jobs.bar.id}"
}
],
"action": "skip",
"prior_state": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/local-[UNIQUE_NAME]/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "foo-[UNIQUE_NAME]",
"queue": {
"enabled": true
},
"tasks": [
{
"run_job_task": {
"job_id": [BAR_ID]
},
"task_key": "run_bar"
}
]
}
}
}
}

=== Job reads during 'bundle plan --plan-mode=local -o json':

>>> print_requests.py --get //jobs/get

=== Deploy --plan with --plan-mode=local plan (warns):

>>> [CLI] bundle deploy --plan-mode=local
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/local-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== Job reads during 'bundle deploy --plan-mode=local':

>>> print_requests.py --get //jobs/get --keep
{
"method": "GET",
"path": "/api/2.2/jobs/get",
"q": {
"job_id": "[BAR_ID]"
}
}

=== Telemetry:
plan_mode_used_local true

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.bar
delete resources.jobs.foo

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/local-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
62 changes: 62 additions & 0 deletions acceptance/bundle/local/basic/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
envsubst '$UNIQUE_NAME' < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve
rm -f out.requests.txt
}
trap cleanup EXIT

# Deploy both jobs so that they are recorded in the local state. foo references
# ${resources.jobs.bar.id}.
trace $CLI bundle deploy
rm -f out.requests.txt

BAR_ID=$(read_id.py bar)

# A --plan-mode=local plan reports no drift without reading the remote state of the jobs:
# no GET is issued. The ${resources.jobs.bar.id} reference still resolves because
# it comes from the local state, not a remote read.
trace $CLI bundle plan --plan-mode=local
title "Job reads during 'bundle plan --plan-mode=local':\n"
trace print_requests.py --get //jobs/get

# Rename bar out-of-band (simulates remote drift). --plan-mode=local plan still shows
# no changes: it never reads remote state, so the rename is invisible to it.
edit_resource.py jobs $BAR_ID <<'EOF'
r["name"] = r["name"] + "-drifted"
EOF
rm -f out.requests.txt

title "After out-of-band rename of bar: --plan-mode=local plan still shows no drift:\n"
trace $CLI bundle plan --plan-mode=local
title "Job reads during --plan-mode=local plan after drift:\n"
trace print_requests.py --get //jobs/get

# The JSON plan is self-describing: plan_mode is "local", so deploy --plan can
# warn before applying it. remote_state is absent (nothing was fetched);
# prior_state carries the last saved local state and is what drift classification
# and apply-time consumers read in local mode.
title "Plan --plan-mode=local as JSON:\n"
trace $CLI bundle plan --plan-mode=local -o json | tee plan.local.json
title "Job reads during 'bundle plan --plan-mode=local -o json':\n"
trace print_requests.py --get //jobs/get

# deploy --plan with a --plan-mode=local plan warns that the plan may miss out-of-band drift.
# READPLAN matrix runs this twice: once applying the saved plan (warns), once without.
title "Deploy --plan with --plan-mode=local plan (warns):\n"
$CLI bundle deploy $(readplanarg plan.local.json) > LOG.deploy_plan 2>&1
# contains.py echoes stdin, so route its passthrough to a LOG file: the deploy
# output (and the --plan-mode=local warning, present only when READPLAN=1) must not leak
# into output.txt or the two matrix variants would diverge.
cat LOG.deploy_plan | contains.py '!panic' '!internal error' > LOG.contains
if [[ -n "$READPLAN" ]]; then
cat LOG.deploy_plan | contains.py 'does not reflect the remote state' > LOG.contains
fi
rm -f out.requests.txt

# A --plan-mode=local deploy likewise does not read remote state up front and reports its use via telemetry.
trace $CLI bundle deploy --plan-mode=local
title "Job reads during 'bundle deploy --plan-mode=local':\n"
trace print_requests.py --get //jobs/get --keep
title "Telemetry:\n"
print_telemetry_bool_values | grep '^plan_mode_used_local '
9 changes: 9 additions & 0 deletions acceptance/bundle/local/basic/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Local = true
Cloud = true
RecordRequests = true
# --plan-mode=local skips the per-resource remote read, which only the direct engine performs.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
# READPLAN=1 feeds the saved local plan to deploy --plan; the warning is asserted in script.
EnvMatrix.READPLAN = ["", "1"]
# databricks.yml is generated at runtime from the template.
Ignore = [".databricks", ".gitignore", "databricks.yml", "plan.local.json"]
15 changes: 15 additions & 0 deletions acceptance/bundle/local/grants-remove/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bundle:
name: grants-remove-$UNIQUE_NAME

resources:
schemas:
myschema:
catalog_name: main
name: schema_grants_remove_$UNIQUE_NAME
grants:
- principal: account users
privileges:
- USE_SCHEMA
- principal: deco-test-user@databricks.com # TO_DELETE
privileges: # TO_DELETE
- USE_SCHEMA # TO_DELETE
3 changes: 3 additions & 0 deletions acceptance/bundle/local/grants-remove/out.test.toml

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

51 changes: 51 additions & 0 deletions acceptance/bundle/local/grants-remove/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/grants-remove-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] bundle deploy --plan-mode=local
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/grants-remove-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== Permissions requests during 'bundle deploy --plan-mode=local':

>>> print_requests.py //permissions
{
"method": "PATCH",
"path": "/api/2.1/unity-catalog/permissions/schema/main.schema_grants_remove_[UNIQUE_NAME]",
"body": {
"changes": [
{
"add": [
"USE_SCHEMA"
],
"principal": "account users",
"remove": [
"ALL_PRIVILEGES"
]
},
{
"principal": "deco-test-user@databricks.com",
"remove": [
"ALL_PRIVILEGES"
]
}
]
}
}

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

This action will result in the deletion of the following UC schemas. Any underlying data may be lost:
delete resources.schemas.myschema

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/grants-remove-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
20 changes: 20 additions & 0 deletions acceptance/bundle/local/grants-remove/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve
rm -f out.requests.txt
}
trap cleanup EXIT

# Full-mode deploy: two principals granted.
trace $CLI bundle deploy
rm -f out.requests.txt

# Remove the second principal by stripping lines marked "# TO_DELETE".
# In local mode entry.RemoteState is nil, so removedGrantPrincipals reads
# from entry.PriorState to compute the removal set from the last saved state.
grep -v '# TO_DELETE' databricks.yml > databricks.yml.new && mv databricks.yml.new databricks.yml

trace $CLI bundle deploy --plan-mode=local
title "Permissions requests during 'bundle deploy --plan-mode=local':\n"
trace print_requests.py //permissions
7 changes: 7 additions & 0 deletions acceptance/bundle/local/grants-remove/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Local = true
Cloud = false
RecordRequests = true
# --plan-mode=local skips the per-resource remote read, which only the direct engine performs.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

Ignore = [".databricks", "databricks.yml"]
16 changes: 16 additions & 0 deletions acceptance/bundle/local/lifecycle-skip/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: lifecycle-skip-$UNIQUE_NAME

workspace:
root_path: ~/.bundle/$UNIQUE_NAME

resources:
clusters:
mycluster:
cluster_name: $UNIQUE_NAME
spark_version: 15.4.x-scala2.12
node_type_id: $NODE_TYPE_ID
instance_pool_id: $TEST_INSTANCE_POOL_ID
num_workers: 1
lifecycle:
started: false
3 changes: 3 additions & 0 deletions acceptance/bundle/local/lifecycle-skip/out.test.toml

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

Loading
Loading