Support tgz artifacts built from a directory or git ref - #6428
Conversation
…git ref
Proposal for AIR-on-DABs code_source parity (see DABs x AIR CLI Alignment).
Extends the existing `artifacts` block so DABs can build the code tarball itself
instead of only uploading a user build command's output:
artifacts:
code_source:
type: tgz # new ArtifactType (was whl|jar)
include: [src/...] # subpaths to pack, gitignore-honored
git: {branch|commit} # snapshot a ref instead of the working tree
files: [{source: ./dist/code.tgz}]
`build` and `git`/`include` are mutually exclusive: either the user's command
produces the tarball (today's behavior) or DABs does. The produced file flows
through the existing artifact upload path, so an ai_runtime_task's
code_source_path pointing at it is uploaded and rewritten to the remote path
with no new reference syntax.
- include: reuses the bundle sync walker (matches file-sync filtering)
- git: shells out to `git archive`
Verified end-to-end on staging (both modes deploy; code_source_path rewritten).
Open for review: this is a proposal to react to, not a finished feature.
jsonschema annotations for the new fields are a follow-up.
Co-authored-by: Isaac
- annotations.yml: descriptions for the new `include`/`git` (+ branch/commit) fields and `tgz` type, so TestRequiredAnnotationsForNewFields passes. - jsonschema.json: regenerated (adds include/git/ArtifactGit/tgz; scoped diff). - validate/strict acceptance golden: artifact type enum now [whl jar tgz]. All bundle/... unit tests, go vet, and the artifacts+validate acceptance subsets pass. Co-authored-by: Isaac
- tarball.go: use errors.New for the no-args git error (perfsprint lint). - jsonschema.json: regenerated post-merge; picks up the variable-ref regex the newer generator emits (validate-generated). Co-authored-by: Isaac
Integration test reportCommit: f2a8fd9
Top 3 slowest tests (at least 2 minutes):
|
ben-hansen-db
left a comment
There was a problem hiding this comment.
Looks good, let's follow the file generation flow for the autogen files
|
|
||
| // tarballFromGit snapshots a git ref via `git archive`, so the archive reflects the | ||
| // committed tree at that ref rather than the working tree. Commit wins over Branch. | ||
| func tarballFromGit(ctx context.Context, b *bundle.Bundle, a *config.Artifact, w io.Writer) error { |
There was a problem hiding this comment.
have we tested equivalence of tarballs from here vs CLI? Note also the tarball changes that landed in CLI recently to respect gitignore
There was a problem hiding this comment.
Made them share code instead of testing equivalence
| b.Metrics.AddBoolValue(metrics.ArtifactFilesIsSet, len(artifact.Files) != 0) | ||
|
|
||
| // A `tgz` artifact with `include`/`git` is built by DABs itself in the build | ||
| // phase (see artifacts.Build). `build` and `git`/`include` are mutually |
There was a problem hiding this comment.
is semantics for include the same as in air cli? There it is relative to code source root, is include relative to bundle root in DABs? Can we make sure that is documented well somewhere?
There was a problem hiding this comment.
include is now relative to the code-source root, same as the air CLI. The reason I decided this is so that convert-to-dabs can map air CLI's include_paths straight across later.
| var EnumFields = map[string][]string{ | ||
| "artifacts.*.executable": {"bash", "sh", "cmd"}, | ||
| "artifacts.*.type": {"whl", "jar"}, | ||
| "artifacts.*.type": {"whl", "jar", "tgz"}, |
There was a problem hiding this comment.
above it says this is autogenerated. Did claude update this file or was it infact autogenerated?
There was a problem hiding this comment.
there's probably a flow for autogenerating this file to follow
There was a problem hiding this comment.
^ claude ran the autogeneration script
- buildTarballArtifact writes to a temp file and renames on success, so a failed git archive / pack no longer leaves a partial tarball at the output path. - Drop the unreachable `.` fallback in tarballFromInclude; include mode is only entered with a non-empty a.Include. Co-authored-by: Isaac <no-reply@databricks.com>
Clarify that DABs `include` is bundle-root-relative and composes files from anywhere in the bundle (e.g. a code dir plus a sibling env file), intentionally broader than the air CLI's code-source-root-relative include. Update the field doc and schema annotation, and add a packing-layer test showing a tarball composed across two directories keeps both entries at bundle-root-relative paths. Co-authored-by: Isaac <no-reply@databricks.com>
The AI Runtime extracts a code_source tarball to /databricks/code_source/<dir>, so the archive must have a single load-bearing top-level directory. The previous include implementation packed bundle-root-relative entries with no such prefix, diverging from both the air CLI and DABs' own aicode directory packer. - Export aicode.BuildCodeSnapshot and reuse it for include mode, so entries nest under the code-source root's directory name (relBase + prefix), gitignore-aware and reproducible — identical layout to a directory code_source_path. - include now selects subpaths of the code-source root (`path`), relative to it, matching the air CLI's include_paths (so convert-to-dabs can translate 1:1). - git mode adds --prefix=<dir>/ so git archive produces the same top-level layout. - Replace the bundle-root-relative field docs/test from the prior commit with the code-source-root semantics and a git-mode prefix test. Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
Disable core.autocrlf in the temp repo and compare trimmed content, so the content assertion doesn't depend on the Windows runner's autocrlf default. Co-authored-by: Isaac <no-reply@databricks.com>
| } | ||
|
|
||
| // codeRoot returns the artifact's code-source root as a sync-root-relative path plus | ||
| // its directory name. dirName is the load-bearing top-level entry the runtime extracts |
| if len(a.Files) != 1 { | ||
| return fmt.Errorf("artifact %q: a tgz artifact needs exactly one `files` entry naming the output path", name) | ||
| } | ||
| out := a.Files[0].Source // made absolute by artifacts.Prepare |
There was a problem hiding this comment.
Why do we need the output path to be specified?
We can interpolate a temporary path, under .databricks for example, such that this file is never included in the archive it produces on the second run.
Artifacts are uploaded separately from regular files. The places that refer to artifacts do path rewriting to make sure the final path we use in the workspace is the right one.
There was a problem hiding this comment.
For now I think this is still necessary. We can mark it as a follow up if you feel it shouldn't be, but right now the BYOT API specifies code_source_path, which is where the user runs their code. Without this output path I think it becomes a little opaque to the user that code_source_path would still say ./dist/code.tgz, but the artifact now lives somewhere else and nothing links them.
I asked claude what it would take to bridge this gap and we would need to let code_source_path reference the artifact by name, but alledgely its not a very easy fix.
There was a problem hiding this comment.
Thanks, yeah I agree with the opaque bit, and that referencing via interpolation is the right solution.
You'd get something like this:
code_source_path: ${artifacts.air_code_source.output}- Make tarball creation independent of the aicode package: inline the tar/gzip packing in bundle/artifacts and revert the aicode export. - Drop the injected code-dir basename prefix. Archive entries are now named relative to the artifact's `path`, so `path: src` + `include: [foo]` and `path: .` + `include: [src/foo]` control the layout explicitly. - Do not apply the bundle-wide sync.include/exclude when packing; only .gitignore filters the walk. - Simplify the temp-file cleanup (drop the redundant renamed flag). Co-authored-by: Isaac <no-reply@databricks.com>
| if len(a.Files) != 1 { | ||
| return fmt.Errorf("artifact %q: a tgz artifact needs exactly one `files` entry naming the output path", name) | ||
| } | ||
| out := a.Files[0].Source // made absolute by artifacts.Prepare |
There was a problem hiding this comment.
Thanks, yeah I agree with the opaque bit, and that referencing via interpolation is the right solution.
You'd get something like this:
code_source_path: ${artifacts.air_code_source.output}Instead of rejecting a git-pinned or include_paths-narrowed code_source, convert
now emits a `tgz` artifact (the DABs artifact snapshotter): DABs builds the
tarball from the git ref / include subset at deploy, and code_source_path points
at the built tarball. Plain snapshots are unchanged — code_source_path stays the
source directory, packaged by the deploy-time aicode mutator.
- snapshot.git.{branch,commit} -> artifact git.{branch,commit}
- snapshot.include_paths -> artifact include (code-source-root-relative, matching
air CLI semantics)
- remote_volume is still rejected (not representable per code source).
This unblocks full-fidelity conversion of the cases convert previously refused.
Stacked on the artifacts tgz/git/include primitive (#6428).
Co-authored-by: Isaac <no-reply@databricks.com>
## Summary `experimental air convert-to-dabs` previously rejected a `code_source.snapshot` that pinned a git ref or narrowed to `include_paths` — it errored and told the user to work around it. Now it translates them into a `tgz` artifact (added in #6428): DABs builds the tarball from the git ref / include subset at deploy, and `code_source_path` points at the built tarball. This closes the conversion gaps for the two cases convert used to refuse. ## Mapping For a snapshot that pins a git ref and/or `include_paths`, convert emits a `tgz` artifact and points `code_source_path` at its output: - `snapshot.git.{branch,commit}` → artifact `git.{branch,commit}` - `snapshot.include_paths` → artifact `include` Archive entries must nest under the code directory's basename (the runtime extracts to `/databricks/code_source/<dir>`), so convert emits `path` = the code dir's parent and `include` = basename-prefixed subpaths: - `root_path: ./src` → `path: ".", include: ["src"]` - `root_path: ./src`, `include_paths: [foo]` → `path: ".", include: ["src/foo"]` A plain snapshot (no git / include_paths) is unchanged: `code_source_path` stays the source directory, packaged at deploy. `remote_volume` is still rejected — a per-source volume isn't representable in a bundle (set `workspace.artifact_path` instead). ## Testing Unit tests assert the emitted artifact (`type`, `path`, `git`/`include`, `files`) and the rewritten `code_source_path` for both the git-ref and include_paths cases; the plain-snapshot path is covered by the existing convert acceptance golden. E2E test: TEST INCLUDE Setup ``` $ rm -rf /tmp/pr2-include && mkdir -p /tmp/pr2-include/src && cd /tmp/pr2-include printf 'print("include ok")\n' > src/train.py printf 'python train.py\n' > src/command.sh printf 'excluded\n' > src/extra.txt cat > train.yaml <<'YAML' experiment_name: pr2_include_demo command: python train.py compute: {accelerator_type: GPU_1xA10, num_accelerators: 1} code_source: type: snapshot snapshot: root_path: ./src include_paths: [train.py, command.sh] YAML ``` convert to dabs ``` $ /tmp/cli-pr2 experimental air convert-to-dabs train.yaml Wrote a Databricks Asset Bundle to .: databricks.yml generated_artifacts/training_config.yaml generated_artifacts/command.sh To deploy and run this workload as a bundle: 1. /tmp/cli-pr2 bundle validate 2. /tmp/cli-pr2 bundle deploy 3. /tmp/cli-pr2 bundle run pr2_include_demo --no-wait bundle deploy uploads the code source and launch scripts automatically. To see what it deployed and where: /tmp/cli-pr2 bundle summary Unlike `air run` (which submits an ephemeral run), bundle deploy creates a persistent job that is not garbage-collected. When you are done, remove the job and its uploaded files with: /tmp/cli-pr2 bundle destroy ``` investigate artifacts: ``` $ cat databricks.yml cat generated_artifacts/command.sh bundle: name: pr2_include_demo sync: paths: - generated_artifacts artifacts: code_source: type: tgz path: . include: - src/train.py - src/command.sh files: - source: ./dist/code_source.tgz targets: dev: mode: development default: true resources: jobs: pr2_include_demo: name: pr2_include_demo tasks: - task_key: pr2_include_demo environment_key: default max_retries: 3 ai_runtime_task: experiment: pr2_include_demo deployments: - command_path: ./generated_artifacts/command.sh compute: accelerator_type: GPU_1xA10 accelerator_count: 1 code_source_path: ./dist/code_source.tgz environments: - environment_key: default spec: environment_version: "4" cd /databricks/code_source/src python train.py% ``` deploy: ``` $ /tmp/cli-pr2 bundle validate && /tmp/cli-pr2 bundle deploy Name: pr2_include_demo Target: dev Workspace: User: v.chen@databricks.com Path: /Workspace/Users/v.chen@databricks.com/.bundle/pr2_include_demo/dev Validation OK! Building code_source... Uploading dist/code_source.tgz... Uploading bundle files to /Workspace/Users/v.chen@databricks.com/.bundle/pr2_include_demo/dev/files... Files: 2 uploaded, 0 deleted Resources: 0 created, 0 changed, 0 deleted, 1 unchanged # v.chen at ip-10-90-20-219 in /tmp/pr2-include (git:) [19:22:29] $ tar tzf dist/code_source.tgz src/command.sh src/train.py ``` run: ``` # v.chen at ip-10-90-20-219 in /tmp/pr2-include (git:) [19:22:34] $ /tmp/cli-pr2 bundle summary Name: pr2_include_demo Target: dev Workspace: User: v.chen@databricks.com Path: /Workspace/Users/v.chen@databricks.com/.bundle/pr2_include_demo/dev Resources: Jobs: pr2_include_demo: Name: [dev v_chen] pr2_include_demo URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/212600874211444?w=6051921418418893 # v.chen at ip-10-90-20-219 in /tmp/pr2-include (git:) [19:22:39] $ /tmp/cli-pr2 bundle run pr2_include_demo --no-wait Run URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/212600874211444/runs/882449706897243?o=6051921418418893 ``` <img width="1421" height="510" alt="image" src="https://github.com/user-attachments/assets/ef824a46-9288-4ef3-aee0-537231949a6e" /> <img width="1043" height="530" alt="image" src="https://github.com/user-attachments/assets/ce9fbb9d-496f-4736-95bb-211a9ee0fe36" /> TEST GIT Setup dirty git: ``` # v.chen at ip-10-90-20-219 in /tmp/pr2-include (git:) [19:22:48] $ rm -rf /tmp/pr2-git && mkdir -p /tmp/pr2-git/src && cd /tmp/pr2-git git init -q -b main printf 'print("git v1 committed")\n' > src/train.py printf 'python train.py\n' > src/command.sh git add -A && git commit -qm "v1" Databricks pre-commit Git Hook V2.5.0 Running secret scanning on changes staged for commit. secret-scan hook completed in 74 ms Unknown project name: None, skipping linting. pre-commit-total hook completed in 90 ms Databricks commit-msg Git Hook V2.5.0 Running secret scanning on commit message. secret-scan hook completed in 23 ms commit-msg-total hook completed in 29 ms # v.chen at ip-10-90-20-219 in /tmp/pr2-git (git:main) [19:26:30] $ printf 'print("v2 UNCOMMITTED - should NOT appear")\n' > src/train.py cat > train.yaml <<'YAML' experiment_name: pr2_git_demo command: python train.py compute: {accelerator_type: GPU_1xA10, num_accelerators: 1} code_source: type: snapshot snapshot: root_path: ./src git: {branch: main} YAML ``` convert to dabs and investigate artifact: ``` $ /tmp/cli-pr2 experimental air convert-to-dabs train.yaml cat databricks.yml Wrote a Databricks Asset Bundle to .: databricks.yml generated_artifacts/training_config.yaml generated_artifacts/command.sh To deploy and run this workload as a bundle: 1. /tmp/cli-pr2 bundle validate 2. /tmp/cli-pr2 bundle deploy 3. /tmp/cli-pr2 bundle run pr2_git_demo --no-wait bundle deploy uploads the code source and launch scripts automatically. To see what it deployed and where: /tmp/cli-pr2 bundle summary Unlike `air run` (which submits an ephemeral run), bundle deploy creates a persistent job that is not garbage-collected. When you are done, remove the job and its uploaded files with: /tmp/cli-pr2 bundle destroy bundle: name: pr2_git_demo sync: paths: - generated_artifacts artifacts: code_source: type: tgz path: . git: branch: main include: - src files: - source: ./dist/code_source.tgz targets: dev: mode: development default: true resources: jobs: pr2_git_demo: name: pr2_git_demo tasks: - task_key: pr2_git_demo environment_key: default max_retries: 3 ai_runtime_task: experiment: pr2_git_demo deployments: - command_path: ./generated_artifacts/command.sh compute: accelerator_type: GPU_1xA10 accelerator_count: 1 code_source_path: ./dist/code_source.tgz environments: - environment_key: default spec: environment_version: "4" ``` deploy: ``` $ /tmp/cli-pr2 bundle validate && /tmp/cli-pr2 bundle deploy Name: pr2_git_demo Target: dev Workspace: User: v.chen@databricks.com Path: /Workspace/Users/v.chen@databricks.com/.bundle/pr2_git_demo/dev Validation OK! Building code_source... Uploading dist/code_source.tgz... Uploading bundle files to /Workspace/Users/v.chen@databricks.com/.bundle/pr2_git_demo/dev/files... Created jobs.pr2_git_demo Files: 2 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged ``` Prove we properly exclude dirty git: ``` # v.chen at ip-10-90-20-219 in /tmp/pr2-git (git:main) [19:28:02] $ mkdir -p /tmp/pr2-git-check && tar xzf dist/code_source.tgz -C /tmp/pr2-git-check grep -R "committed" /tmp/pr2-git-check/src/train.py # -> "git v1 committed" grep -R "UNCOMMITTED" /tmp/pr2-git-check/src/train.py || echo "OK: uncommitted change correctly excluded" print("git v1 committed") OK: uncommitted change correctly excluded ``` run: ``` $ /tmp/cli-pr2 bundle summary Name: pr2_git_demo Target: dev Workspace: User: v.chen@databricks.com Path: /Workspace/Users/v.chen@databricks.com/.bundle/pr2_git_demo/dev Resources: Jobs: pr2_git_demo: Name: [dev v_chen] pr2_git_demo URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/235795448342543?w=6051921418418893 # v.chen at ip-10-90-20-219 in /tmp/pr2-git (git:main) [19:28:47] $ /tmp/cli-pr2 bundle run pr2_git_demo --no-wait Run URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/235795448342543/runs/137864060324526?o=6051921418418893 ``` <img width="1437" height="539" alt="image" src="https://github.com/user-attachments/assets/0474aba1-2b4c-4cf1-b646-34a785efdc88" /> <img width="1035" height="494" alt="image" src="https://github.com/user-attachments/assets/5a72ec33-03e7-4df1-9385-cdd942cfadf4" /> --------- Co-authored-by: Isaac <no-reply@databricks.com>
…databricks#6494) databricks#6110 <- basically reverting this pr A local-directory code_source_path is now turned into a `tgz` artifact and built and uploaded through the standard artifact path, instead of the aicode mutator splicing a content-addressed tarball onto the sync root. The tgz artifact is the single packing mechanism. - aicode.PackageCodeSource now synthesizes a `tgz` artifact per local-dir code_source_path (path = the dir's parent, include = its basename, so archive entries nest under the basename for the /databricks/code_source/<dir> layout) and rewrites code_source_path to the built tarball. It runs before artifacts.Prepare (initialize) instead of in the build phase. - Remove the sync-root overlay, the content-addressed packer (buildCodeSnapshot), bundle.HasAiRuntimeCodeSnapshot, bundle.AiCodeSnapshotDir, and the validateSnapshotDir guards that only existed for the overlay. - Behavior change: only .gitignore filters the packaged files now; the bundle-wide sync.include/exclude no longer apply to a code artifact. - Keep the git_source / immutable-folder / source-linked / for_each rejections. ## Why databricks#6428 We recently merged a DABs native uploader, which supercedes this mutator workaround ## Tests Unit tests E2E test: Setup: ``` # v.chen at ip-10-90-20-219 in /tmp/pr2-e2e (git:) [18:59:50] $ cd ~/.worktrees/cli-rmaicode && go build -o /tmp/cli-pr3 . && cd - rm -rf /tmp/pr3-e2e && mkdir -p /tmp/pr3-e2e/src && cd /tmp/pr3-e2e printf 'print("train ok")\n' > src/train.py # hand-authored command.sh cds into the extracted code dir itself printf 'cd /databricks/code_source/src\npython train.py\n' > src/command.sh cat > databricks.yml <<'YAML' bundle: name: pr3-code-source-demo resources: jobs: train: name: "[${bundle.target}] pr3 demo" tasks: - task_key: train environment_key: default ai_runtime_task: experiment: pr3_demo code_source_path: ./src deployments: - command_path: src/command.sh compute: {accelerator_type: GPU_1xA10, accelerator_count: 1} environments: - environment_key: default spec: environment_version: "5" targets: dev: {mode: development, default: true} YAML export DATABRICKS_CONFIG_PROFILE=e2-dogfood DATA /tmp/pr2-e2e # v.chen at ip-10-90-20-219 in /tmp/pr3-e2e (git:) [18:59:59] $ /tmp/cli-pr3 bundle deploy Building air_code_source_src... Uploading .databricks/air_code_source/air_code_source_src.tar.gz... Uploading bundle files to /Workspace/Users/v.chen@databricks.com/.bundle/pr3-code-source-demo/dev/files... Created jobs.train Files: 3 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged ``` Show deploy works: ``` # v.chen at ip-10-90-20-219 in /tmp/pr3-e2e (git:) [19:00:14] $ /tmp/cli-pr3 bundle deploy Building air_code_source_src... Uploading .databricks/air_code_source/air_code_source_src.tar.gz... Uploading bundle files to /Workspace/Users/v.chen@databricks.com/.bundle/pr3-code-source-demo/dev/files... Files: 0 uploaded, 0 deleted Resources: 0 created, 0 changed, 0 deleted, 1 unchanged # v.chen at ip-10-90-20-219 in /tmp/pr3-e2e (git:) [19:01:00] ``` Investigate what is in tar: ``` $ tar tzf .databricks/air_code_source/air_code_source_src.tar.gz src/command.sh src/train.py # v.chen at ip-10-90-20-219 in /tmp/pr3-e2e (git:) [19:01:29] $ ``` bundle summary: ``` $ /tmp/cli-pr3 bundle summary Name: pr3-code-source-demo Target: dev Workspace: User: v.chen@databricks.com Path: /Workspace/Users/v.chen@databricks.com/.bundle/pr3-code-source-demo/dev Resources: Jobs: train: Name: [dev v_chen] [dev] pr3 demo URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/1015159229445184?w=6051921418418893 # v.chen at ip-10-90-20-219 in /tmp/pr3-e2e (git:) [19:01:51] $ ``` bundle run: ``` $ /tmp/cli-pr3 bundle run train --no-wait Run URL: https://e2-dogfood.staging.cloud.databricks.com/jobs/1015159229445184/runs/174551477706538?o=6051921418418893 ``` Run details: <img width="1512" height="821" alt="image" src="https://github.com/user-attachments/assets/b18e41ce-f861-42be-b247-b5af50b6ce43" /> <img width="1495" height="672" alt="image" src="https://github.com/user-attachments/assets/193f8e8e-97c0-4e0b-a12f-e5671c19f102" /> --------- Co-authored-by: Isaac <no-reply@databricks.com>
Adds a tgz artifact type that packages source files into a gzipped tarball as part of bundle deploy, alongside the existing whl and jar types. Unlike those, DABs builds the tarball itself — there is no user build command.
Two sources are supported:
Archive entries are named relative to the artifact's path, so the layout is controlled by config. The built tarball is uploaded and referenced like any other artifact file.
Co-authored-by: Isaac
Changes
Why
Tests
Show that the include field is respected, we notice the
./dist/code.tgzis properly uploaded.Show that git refs are respected: