Skip to content

Package a local directory code_source_path for AI Runtime tasks - #6110

Open
vinchenzo-db wants to merge 9 commits into
mainfrom
air-code-source-dir-rebase
Open

Package a local directory code_source_path for AI Runtime tasks#6110
vinchenzo-db wants to merge 9 commits into
mainfrom
air-code-source-dir-rebase

Conversation

@vinchenzo-db

@vinchenzo-db vinchenzo-db commented Jul 31, 2026

Copy link
Copy Markdown

Let ai_runtime_task.code_source_path point at a local directory, not just a pre-built tarball. A new aicode mutator (run in the build phase, before libraries.ExpandGlobReferences/ReplaceWithRemotePath) detects a local-directory value, packages it into a reproducible, content-addressed tarball — honoring .gitignore and the top-level sync.include/exclude globs — uploads it to the user's ~/.air/repo_snapshots directory, and rewrites code_source_path to the uploaded (de-/Workspace-prefixed) path. Because it runs first, PR #5922's artifact-style collection then sees an already-remote path and skips it, so the two compose: a directory is packaged by the CLI, a pre-built .tgz still flows through the artifact path.

Also synthesizes a requirements.yaml next to the task's command_path from the job's serverless environments[] spec, so the AI Runtime harness sets up the workload environment. command_path translation itself is provided by #5922.

Verified end-to-end on a live workspace: a plain bundle deploy of a directory code_source_path yields a job whose run terminates SUCCESS; sync.exclude and .gitignore entries are absent from the uploaded snapshot; unchanged code skips re-upload.

Co-authored-by: Isaac

Changes

Adds an aicode bundle mutator that packages a local code directory referenced by an AI Runtime task's code_source_path and uploads it during bundle deploy, then wires it into the build/initialize phases.

  • bundle/config/mutator/aicode/package_upload.go — PackageAndUpload (build-phase mutator): finds every ai_runtime_task.code_source_path (direct and under for_each_task) that points at a local directory, packages it into a reproducible, content-addressed .tar.gz (.git/gitignored files excluded, honoring the top-level sync.include/exclude), uploads it to the user's ~/.air/repo_snapshots directory, and rewrites code_source_path to the uploaded remote path. The archive name embeds the SHA-256 of the tarball, so an unchanged directory resolves to the same remote path and re-uploads are skipped.
  • bundle/config/mutator/aicode/snapshot_package.go — the reproducible tar builder (fixed epoch on every entry so identical content ⇒ identical bytes ⇒ identical hash).
  • bundle/config/mutator/aicode/requirements.go — SynthesizeRequirements: writes a requirements.yaml next to each task's translated command_path, derived from the job's serverless environments[] spec, so the AI Runtime harness can set up the workload environment.
  • bundle/config/mutator/aicode/validate.go — Validate (read-only, initialize-phase): surfaces misconfigurations early (local code_source_path combined with git_source, or with experimental.immutable_folder) with actionable messages, performing no uploads.
  • Directory-vs-file discrimination (package_upload.go + validate.go): both mutators only claim a code_source_path that resolves to an existing directory. A local file (a pre-built tarball delivered via an artifacts block) or a not-yet-existing path is left untouched so it flows through the standard artifact-upload path. This is what lets the two paths compose.
  • bundle/phases/{build,initialize}.go — runs PackageAndUpload + SynthesizeRequirements in the build phase before libraries.ExpandGlobReferences/ReplaceWithRemotePath, and Validate in the initialize phase.
  • Acceptance test bundle/ai_runtime_task/local_code_source covering the full package → upload → rewrite → requirements-synthesis flow, plus unit tests for each mutator (including the directory/file/remote skip cases).

Why

The SDK jobs.AiRuntimeTask.code_source_path field expects a workspace or UC-volume path to an uploaded code archive, and its doc comment states the CLI is responsible for packaging the user's local code into that archive. Nothing in DABs did that yet: a user pointing code_source_path at a local directory had no way to bundle deploy a runnable AI Runtime job.

This mutator implements that contract for bundles. It deliberately reuses DABs' existing local-artifact upload plumbing and runs before ReplaceWithRemotePath, so the two upload paths compose without special-casing: a directory is packaged and uploaded by this mutator (which then presents an already-remote path that the artifact collector skips), while a pre-built tarball delivered via an artifacts block still flows through the artifact path as a file. Content-addressing keeps redeploys of unchanged code a no-op. SynthesizeRequirements closes the loop so the deployed workload actually has its environment set up — without it the run fails during setup.

Tests

End-to-end validation

Verified on a live A10 staging workspace (dbc-04ac0685-8857) that the aicode mutator packages a local-directory code_source_path at bundle deploy and that the deployed AI Runtime workload runs to SUCCESS on real GPU. Covered all three code-source shapes the mutator must handle, plus the artifact-tarball path it must not claim.

What was validated
Seam: PackageAndUpload claims a local directory
Expected: dir → content-addressed .tar.gz, uploaded to ~/.air/repo_snapshots, code_source_path rewritten to
the remote archive
Result: ✅
────────────────────────────────────────
Seam: PackageAndUpload skips a local file
Expected: a pre-built tarball (artifacts block) flows through the standard artifact upload, not aicode
packaging
Result: ✅ (bundle/artifacts/ai_runtime_code_source acceptance test)
────────────────────────────────────────
Seam: command_path translation
Expected: rewritten to its absolute synced workspace path
Result: ✅
────────────────────────────────────────
Seam: SynthesizeRequirements
Expected: writes requirements.yaml next to command_path, derived from environments[].spec
Result: ✅
────────────────────────────────────────
Seam: GPU workload
Expected: extracts code, installs deps, runs on A10, exits SUCCESS
Result: ✅

Deployed ai_runtime_task after bundle deploy (mutator output)

{
"code_source_path": "/Users//.air/repo_snapshots/code_source/code_source_5659f5b55c3427b1.tar.gz",
"command_path": "/Workspace/Users//.bundle/convert-e2e-smoke/dev/files/command.sh"
}

code_source_path was ./code_source (a local directory) in the bundle — the mutator packaged it, uploaded the content-addressed archive, and rewrote the field to the remote path.

Live GPU runs (all SUCCESS on NVIDIA A10G)

┌────────────────────┬───────────────────────────┬───────────┬─────────────────────────────────────────┐
│      Scenario      │     code_source shape     │   Run     │          Workload log evidence          │
│                    │                           │  result   │                                         │
├────────────────────┼───────────────────────────┼───────────┼─────────────────────────────────────────┤
│                    │                           │ ✅        │ cuda available: True · device: NVIDIA   │
│ Working tree       │ local dir                 │ SUCCESS   │ A10G · matmul checksum 1131894.0 · ===  │
│                    │                           │ (242s)    │ SUCCESS ===                             │
├────────────────────┼───────────────────────────┼───────────┼─────────────────────────────────────────┤
│                    │ local dir (materialized   │ ✅        │ ran the committed code, not the dirty   │
│ Git-pinned commit  │ from a pinned SHA)        │ SUCCESS   │ working tree · === SUCCESS ===          │
│                    │                           │ (192s)    │                                         │
├────────────────────┼───────────────────────────┼───────────┼─────────────────────────────────────────┤
│ Requirements-file  │ local dir + deps in       │ ✅        │ torch 2.13.0+cu130, numpy 2.1.3         │
│ deps               │ environments[].spec       │ SUCCESS   │ installed via synthesized               │
│                    │                           │ (272s)    │ requirements.yaml · === SUCCESS ===     │
└────────────────────┴───────────────────────────┴───────────┴─────────────────────────────────────────┘

Reproduce

# 1. Author a run YAML with a local code_source directory
cat > train.yaml <<'YAML'
experiment_name: aicode-e2e
command: cd "$CODE_SOURCE_PATH" && python train.py
compute: {accelerator_type: GPU_1xA10, num_accelerators: 1}
environment: {version: 5, dependencies: [torch]}
code_source: {type: snapshot, snapshot: {root_path: ./src}}
YAML
mkdir -p src && echo 'import torch; print("cuda:", torch.cuda.is_available()); print("=== SUCCESS ===")' > src/train.py

# 2. Convert to a bundle (emits code_source_path: ./code_source, a local directory)
databricks experimental air convert-to-dabs train.yaml --output-dir bundle
cd bundle

# 3. Deploy — the aicode mutator packages the directory, uploads it, rewrites code_source_path,
#    and synthesizes requirements.yaml next to command_path
databricks bundle deploy

# 4. Run on GPU and confirm SUCCESS
databricks bundle run aicode-e2e

Let ai_runtime_task.code_source_path point at a local directory, not just a
pre-built tarball. A new aicode mutator (run in the build phase, before
libraries.ExpandGlobReferences/ReplaceWithRemotePath) detects a local-directory
value, packages it into a reproducible, content-addressed tarball — honoring
.gitignore and the top-level sync.include/exclude globs — uploads it to the
user's ~/.air/repo_snapshots directory, and rewrites code_source_path to the
uploaded (de-/Workspace-prefixed) path. Because it runs first, PR #5922's
artifact-style collection then sees an already-remote path and skips it, so the
two compose: a directory is packaged by the CLI, a pre-built .tgz still flows
through the artifact path.

Also synthesizes a requirements.yaml next to the task's command_path from the
job's serverless environments[] spec, so the AI Runtime harness sets up the
workload environment. command_path translation itself is provided by #5922.

Verified end-to-end on a live workspace: a plain bundle deploy of a directory
code_source_path yields a job whose run terminates SUCCESS; sync.exclude and
.gitignore entries are absent from the uploaded snapshot; unchanged code skips
re-upload.

Co-authored-by: Isaac
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/bundle/ - needs approval

12 files changed
Suggested: @denik
Also eligible: @janniklasrose, @shreyas-goenka, @pietern, @andrewnester, @lennartkats-db, @anton-107

/bundle/ - needs approval

10 files changed
Suggested: @denik
Also eligible: @janniklasrose, @shreyas-goenka, @pietern, @andrewnester, @lennartkats-db, @anton-107

General files (require maintainer)

5 files changed
Based on git history:

  • @denik -- recent work in bundle/phases/, .nextchanges/bundles/, acceptance/bin/

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db) can approve all areas.
See OWNERS for ownership rules.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: e306181

Run: 30670027366

Env 🟨​KNOWN 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🟨​ aws linux 3 1 4 313 1068 5:38
🟨​ aws windows 3 1 4 315 1066 5:58
🟨​ azure linux 3 1 4 313 1067 5:58
🟨​ azure windows 3 1 4 315 1065 6:58
💚​ gcp linux 1 5 313 1068 4:14
💚​ gcp windows 1 5 315 1066 4:36
8 interesting tests: 4 SKIP, 3 KNOWN, 1 RECOVERED
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestFetchRepositoryInfoAPI_FromRepo 🟨​K 🟨​K 🟨​K 🟨​K 🙈​S 🙈​S
🟨​ TestFetchRepositoryInfoAPI_FromRepo/root 🟨​K 🟨​K 🟨​K 🟨​K
🟨​ TestFetchRepositoryInfoAPI_FromRepo/subdir 🟨​K 🟨​K 🟨​K 🟨​K
Top 3 slowest tests (at least 2 minutes):
duration env testname
3:35 azure windows TestAccept
3:21 gcp windows TestAccept
3:11 aws windows TestAccept

The aicode mutator packages a local directory code_source_path at deploy. A
pre-built tarball delivered via an `artifacts` block (code_source_path pointing
at a local *file*) must instead flow through the standard artifact-upload path,
exactly as the two-path design intends. Validate rejected such a path outright
("code_source_path not found" — the artifact tarball does not exist yet at
initialize time), breaking the existing bundle/artifacts/ai_runtime_code_source
acceptance test.

Both Validate and collectLocalCodeSources now skip any local code_source_path
that does not resolve to an existing directory, leaving it for the artifact
uploader. The packaging-specific constraints (git_source, immutable_folder) only
apply once the path is confirmed to be a directory this mutator will package.

Co-authored-by: Isaac
Comment thread acceptance/bundle/ai_runtime_task/local_code_source/src/.gitignore
Comment thread acceptance/bundle/ai_runtime_task/local_code_source/databricks.yml
Comment thread acceptance/bundle/ai_runtime_task/local_code_source/script Outdated
Comment thread acceptance/bundle/ai_runtime_task/local_code_source/script Outdated
Comment thread bundle/config/mutator/aicode/validate.go Outdated
Comment thread bundle/config/mutator/aicode/package_upload.go Outdated
Comment thread bundle/config/mutator/aicode/package_upload.go Outdated
Comment thread acceptance/bundle/ai_runtime_task/local_code_source/databricks.yml
Comment thread bundle/config/mutator/aicode/package_upload.go Outdated
Comment thread bundle/config/mutator/aicode/requirements.go Outdated
- Remove the never-set `client` filer field from packageAndUpload and
  synthesizeRequirements (only ever nil); build the workspace filer directly.
- Read the current user directly (resolved in the initialize phase) instead of
  the defensive nil-guarded helper.
- Surface a stat error other than not-exist (e.g. unreadable directory) instead
  of silently treating it as "not a directory to package"; shared isExistingDir
  helper used by collect and Validate.
- Pass GitSource into validateTask (nil-check inside) to keep validation logic
  contained.
- Collect a single direct-task code_source_path pattern, matching Validate and
  SynthesizeRequirements (for_each_task is unsupported until all three gain it).
- SynthesizeRequirements writes requirements.yaml next to every deployment's
  command_path (deduped by directory), not just the first.

Co-authored-by: Isaac
Reworks the local_code_source acceptance test per review:
- Assert the uploaded tarball's contents via list_code_snapshot.py: gitignored
  files (ignored_by_git.txt, data/) are excluded, *.log is excluded via
  sync.exclude, and data/model.bin is force-included via sync.include.
- Use print_requests.py instead of hand-rolled jq (testing.md rule); extend its
  --del-body to also strip the top-level raw_body so the binary tarball upload
  doesn't dump into the golden.
- Add a cache-miss case: editing a file changes the content hash and re-uploads.

Co-authored-by: Isaac
vinchenzo-db and others added 5 commits July 31, 2026 12:53
print_requests.py path filters and the workspace export in list_code_snapshot.py
were rewritten by Git Bash's MSYS path conversion on Windows (e.g. /repo_snapshots/
-> C:/Program Files/Git/repo_snapshots/), failing the test. Use // leading
filters (matching the existing convention) and set MSYS_NO_PATHCONV for the export.

Co-authored-by: Isaac
Split subprocess.run kwargs onto separate lines to satisfy ruff format.

Co-authored-by: Isaac
Reworks PackageAndUpload to place the code snapshot inside the bundle instead of
uploading it to a shared ~/.air/repo_snapshots workspace cache. The mutator now
writes the content-addressed tarball into the bundle's sync tree (.air_snapshots/)
and rewrites code_source_path to the workspace path it will occupy once synced; it
performs no workspace write itself. Normal bundle file sync uploads it during the
deploy phase.

Why:
- Build phase must not mutate the workspace (it runs before `bundle plan`). The
  old filer upload violated that; now the only build-phase work is local
  prepare-and-archive, and the upload happens in the deploy phase via file sync.
- No deploy-time side-effect outside the bundle: `bundle destroy` removes the
  snapshot like any other bundle file, so the home-dir cache can no longer
  compound across bundles/deploys.
- Dedup is preserved: the name is content-addressed and bundle file sync is
  incremental, so unchanged code keeps the same synced path and is not re-uploaded.

Tradeoff: the cache is now per-bundle rather than shared across bundles. See the
package doc for the (rejected) TranslatePaths alternative and why archiving stays
in the build phase.

Co-authored-by: Isaac
The snapshot is now injected into the bundle sync root as an in-memory overlay
file rather than written under the working tree. This keeps the deploy artifact
inside the bundle (synced to <root>/files/.air_snapshots/, removed by
`bundle destroy`, content-addressed so unchanged code is not re-uploaded) while
leaving the user's working tree clean — no generated .tar.gz appears in their
checkout.

Adds libs/vfs.Overlay: a vfs.Path wrapper that serves a set of in-memory files in
addition to a base path, participating in Open/Stat/ReadDir/ReadFile and fs.WalkDir
so bundle file sync uploads the overlaid files transparently. PackageAndUpload
builds each snapshot in memory and swaps b.SyncRoot for an overlay carrying them.

Co-authored-by: Isaac
@ben-hansen-db
ben-hansen-db self-requested a review July 31, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants