Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b685ec2
Immutable folder support in DABs
andrewnester May 15, 2026
a429b26
remove unused snapshot path method
andrewnester May 28, 2026
e7c1968
added an acceptance test
andrewnester May 28, 2026
67914d0
fix for notebook import
andrewnester May 28, 2026
549492a
removed unused function
andrewnester May 28, 2026
aedfdb0
fix schema + unit test
andrewnester Jun 1, 2026
eddec61
use immutable_folder config
andrewnester Jun 9, 2026
27a6b02
Merge branch 'main' into demo-immutable
andrewnester Jun 9, 2026
4a9bcd9
remove merge conflict
andrewnester Jun 9, 2026
ebd26ea
fix empty artifact path + tests
andrewnester Jun 9, 2026
5efe1da
fixed test config
andrewnester Jun 9, 2026
6215a49
fixes
andrewnester Jun 18, 2026
7b44126
Merge branch 'main' into demo-immutable
andrewnester Jun 18, 2026
be6dec3
fix fmt
andrewnester Jun 18, 2026
0f1ed52
fix annotations
andrewnester Jun 18, 2026
d978559
fix lint
andrewnester Jun 18, 2026
9a6c898
do not call set permissions on immutable ws root
andrewnester Jun 18, 2026
079998b
addressed feedback
andrewnester Jun 22, 2026
c6b1ff5
no destroy + acl + fix for empty path
andrewnester Jun 23, 2026
3a0ec41
pr feedback
andrewnester Jun 23, 2026
b9f67f7
fixed app deploying
andrewnester Jun 23, 2026
01136f5
use snapshot_path veriable prefixes and move to experimetal
andrewnester Jun 23, 2026
6e9aa77
Merge branch 'main' into demo-immutable
andrewnester Jun 24, 2026
e26f085
addressed feedback
andrewnester Jun 24, 2026
e2a5b2a
simplify and clean up
andrewnester Jun 24, 2026
6f3f077
update out.test.toml
andrewnester Jun 24, 2026
5cefc2f
comment
andrewnester Jun 24, 2026
55acaf4
fix test on windows
andrewnester Jun 24, 2026
60e41d3
added client side validation for size of upload
andrewnester Jun 24, 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
22 changes: 14 additions & 8 deletions acceptance/bin/print_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
If argument starts with ! then it's a negation filter.

Examples:
print_requests.py //jobs # Show non-GET requests with /jobs in path
print_requests.py --get //jobs # Show all requests with /jobs in path
print_requests.py --sort '^//import-file/' # Show non-GET requests, exclude /import-file/, sort output
print_requests.py --keep //jobs # Show requests and do not delete out.requests.json afterwards
print_requests.py //jobs # Show non-GET requests with /jobs in path
print_requests.py --get //jobs # Show all requests with /jobs in path
print_requests.py --sort '^//import-file/' # Show non-GET requests, exclude /import-file/, sort output
print_requests.py --keep //jobs # Show requests and do not delete out.requests.json afterwards
print_requests.py //api/2.0/repos/snapshots --method DELETE # Show only DELETE to that path

This replaces custom jq wrappers like:
jq --sort-keys 'select(.method != "GET" and (.path | contains("/jobs")))' < out.requests.txt
Expand Down Expand Up @@ -123,7 +124,7 @@ def read_json_many(s):
assert result == [{"method": "GET"}, {"method": "POST"}], result


def filter_requests(requests, path_filters, include_get, should_sort, unique=False):
def filter_requests(requests, path_filters, include_get, should_sort, unique=False, method_filter=None):
"""Filter requests based on method and path filters."""
positive_filters = []
negative_filters = []
Expand All @@ -138,8 +139,12 @@ def filter_requests(requests, path_filters, include_get, should_sort, unique=Fal

filtered_requests = []
for req in requests:
# Skip GET requests unless include_get is True
if req.get("method") == "GET" and not include_get:
if method_filter:
# --method overrides the default GET exclusion
if req.get("method") != method_filter:
continue
elif req.get("method") == "GET" and not include_get:
# Skip GET requests unless include_get is True
continue

# Apply path filters
Expand Down Expand Up @@ -186,6 +191,7 @@ def main():
action="store_true",
help="Collapse consecutive duplicate requests (like uniq), e.g. repeated GET polls",
)
parser.add_argument("--method", metavar="METHOD", help="Only show requests with this HTTP method (e.g. DELETE)")
parser.add_argument("--oneline", action="store_true", help="Print output with one request per line")
parser.add_argument(
"--del-body",
Expand Down Expand Up @@ -217,7 +223,7 @@ def main():
return

requests = read_json_many(data)
filtered_requests = filter_requests(requests, args.path_filters, args.get, args.sort, args.unique)
filtered_requests = filter_requests(requests, args.path_filters, args.get, args.sort, args.unique, args.method)

for req in filtered_requests:
body = req.get("body")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
bundle:
name: test-bundle-immutable-no-artifacts-$UNIQUE_NAME

experimental:
immutable_folder: true

resources:
jobs:
my_job:
name: my job
tasks:
- task_key: spark_python_task
spark_python_task:
python_file: ./src/main.py
environment_key: env
- task_key: notebook_task
notebook_task:
notebook_path: ./src/notebook.py
base_parameters:
path: ${workspace.file_path}/some_path


environments:
- environment_key: env
spec:
environment_version: "4"
3 changes: 3 additions & 0 deletions acceptance/bundle/deploy/immutable-no-artifacts/out.test.toml

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

33 changes: 33 additions & 0 deletions acceptance/bundle/deploy/immutable-no-artifacts/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

>>> [CLI] bundle validate
Name: test-bundle-immutable-no-artifacts-[UNIQUE_NAME]
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle-immutable-no-artifacts-[UNIQUE_NAME]/default

Validation OK!

>>> [CLI] bundle deploy
Uploading immutable bundle snapshot...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/files/src/main.py"

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/files/src/notebook"

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/files/some_path"

>>> [CLI] bundle destroy --auto-approve
Comment thread
andrewnester marked this conversation as resolved.
The following resources will be deleted:
delete resources.jobs.my_job

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

Deleting files...
Destroy complete!
18 changes: 18 additions & 0 deletions acceptance/bundle/deploy/immutable-no-artifacts/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
rm -f out.requests.txt
}
trap cleanup EXIT

trace $CLI bundle validate
trace $CLI bundle deploy


# Get a job and check that task paths point into the snapshot
JOB_ID=$($CLI bundle summary -o json | jq -r '.resources.jobs.my_job.id')
trace $CLI jobs get $JOB_ID | jq '.settings.tasks' | jq '.[] | select(.spark_python_task != null) | .spark_python_task.python_file'
trace $CLI jobs get $JOB_ID | jq '.settings.tasks' | jq '.[] | select(.notebook_task != null) | .notebook_task.notebook_path'
trace $CLI jobs get $JOB_ID | jq '.settings.tasks' | jq '.[] | select(.notebook_task != null) | .notebook_task.base_parameters.path'

trace $CLI bundle destroy --auto-approve
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Hello from Spark Python Task!")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Databricks notebook source

print("Hello from Notebook Task!")
20 changes: 20 additions & 0 deletions acceptance/bundle/deploy/immutable-no-artifacts/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Local = true
Cloud = false # Temporary disable cloud tests until the API is fully available
RecordRequests = true

# immutable_folder only works with the direct engine.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

Ignore = [
"databricks.yml",
".databricks",
".venv",
"script",
"*.pyc",
]

# Normalize the content-addressed snapshot hash so it doesn't need to be
# hardcoded in output.txt and the test stays stable across file changes.
[[Repls]]
Old = '[0-9a-f]{64}'
New = '[SNAPSHOT_HASH]'
34 changes: 34 additions & 0 deletions acceptance/bundle/deploy/immutable/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
bundle:
name: test-bundle-immutable-$UNIQUE_NAME

experimental:
immutable_folder: true

artifacts:
python_artifact:
type: whl
build: uv build --wheel

resources:
jobs:
my_job:
name: my job
tasks:
- task_key: spark_python_task
spark_python_task:
python_file: ./src/main.py
environment_key: env
- task_key: notebook_task
notebook_task:
notebook_path: ./src/notebook.py
- task_key: python_wheel_task
python_wheel_task:
package_name: immutable
entry_point: main
environment_key: env
environments:
- environment_key: env
spec:
environment_version: "4"
dependencies:
- ./dist/*.whl
3 changes: 3 additions & 0 deletions acceptance/bundle/deploy/immutable/out.test.toml

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

62 changes: 62 additions & 0 deletions acceptance/bundle/deploy/immutable/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

>>> [CLI] bundle validate
Name: test-bundle-immutable-[UNIQUE_NAME]
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle-immutable-[UNIQUE_NAME]/default

Validation OK!

>>> [CLI] bundle plan -o json
Building python_artifact...
[
{
"notebook_task": {
"notebook_path": "${workspace.snapshot_path}/src/files/src/notebook"
},
"task_key": "notebook_task"
},
{
"environment_key": "env",
"python_wheel_task": {
"entry_point": "main",
"package_name": "immutable"
},
"task_key": "python_wheel_task"
},
{
"environment_key": "env",
"spark_python_task": {
"python_file": "${workspace.snapshot_path}/src/files/src/main.py"
},
"task_key": "spark_python_task"
}
]

>>> [CLI] bundle deploy
Building python_artifact...
Uploading immutable bundle snapshot...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/files/src/main.py"

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/files/src/notebook"

>>> [CLI] jobs get [NUMID]
[
"/Workspace/Users/[UUID]/.snapshots/[UUID]/[SNAPSHOT_HASH]/src/artifacts/.internal/immutable-0.0.1-py3-none-any.whl"
]

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

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

Deleting files...
Destroy complete!
34 changes: 34 additions & 0 deletions acceptance/bundle/deploy/immutable/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[project]
name = "immutable"
version = "0.0.1"
authors = [{ name = "andrew.nester@databricks.com" }]
requires-python = ">=3.10,<3.13"
dependencies = [
# Any dependencies for jobs and pipelines in this project can be added here
# See also https://docs.databricks.com/dev-tools/bundles/library-dependencies
#
# LIMITATION: for pipelines, dependencies are cached during development;
# add dependencies to the 'environment' section of your pipeline.yml file instead
]

[dependency-groups]
dev = [
"pytest",
"ruff",
"databricks-dlt",
"databricks-connect>=15.4,<15.5",
"ipykernel",
]

[project.scripts]
main = "immutable.main:main"

[build-system]
requires = ["setuptools>=40.8.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.ruff]
line-length = 120
19 changes: 19 additions & 0 deletions acceptance/bundle/deploy/immutable/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
envsubst < databricks.yml.tmpl > databricks.yml
cleanup() {
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

trace $CLI bundle validate
trace $CLI bundle plan -o json | jq '.plan["resources.jobs.my_job"].new_state.value.tasks'
trace $CLI bundle deploy


# Get a job and check that task paths are immutable
JOB_ID=$($CLI bundle summary -o json | jq -r '.resources.jobs.my_job.id')
trace $CLI jobs get $JOB_ID | jq '.settings.tasks' | jq '.[] | select(.spark_python_task != null) | .spark_python_task.python_file'
trace $CLI jobs get $JOB_ID | jq '.settings.tasks' | jq '.[] | select(.notebook_task != null) | .notebook_task.notebook_path'
trace $CLI jobs get $JOB_ID | jq '.settings.environments[0].spec.dependencies'

# Redirect run output to a log file — the real workspace produces different output than the local test server.
$CLI bundle run my_job &> LOG.run
Empty file.
6 changes: 6 additions & 0 deletions acceptance/bundle/deploy/immutable/src/immutable/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def main():
print("Hello from Python Wheel Task!")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions acceptance/bundle/deploy/immutable/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Hello from Spark Python Task!")
3 changes: 3 additions & 0 deletions acceptance/bundle/deploy/immutable/src/notebook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Databricks notebook source

print("Hello from Notebook Task!")
21 changes: 21 additions & 0 deletions acceptance/bundle/deploy/immutable/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Local = true
Cloud = false # Temporary disable cloud tests until the API is fully available

# immutable_folder only works with the direct engine.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

Ignore = [
"dist",
"build",
"databricks.yml",
".databricks",
".venv",
"script",
"*.pyc",
"src/*.egg-info",
]

[[Repls]]
# Replace snapshot hash with SNAPSHOT_HASH
Old = "[0-9a-f]{64}"
New = "[SNAPSHOT_HASH]"
3 changes: 3 additions & 0 deletions acceptance/bundle/resources/apps/immutable/app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import streamlit as st

st.write("hello")
11 changes: 11 additions & 0 deletions acceptance/bundle/resources/apps/immutable/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
bundle:
name: test-bundle-immutable-app-$UNIQUE_NAME

experimental:
immutable_folder: true

resources:
apps:
my_app:
name: my-immutable-app
source_code_path: ./app
3 changes: 3 additions & 0 deletions acceptance/bundle/resources/apps/immutable/out.test.toml

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

Loading
Loading