Skip to content

air: add convert-to-dabs (run YAML -> Databricks Asset Bundle) - #6111

Merged
vinchenzo-db merged 17 commits into
mainfrom
air-convert-to-dabs-main
Aug 7, 2026
Merged

air: add convert-to-dabs (run YAML -> Databricks Asset Bundle)#6111
vinchenzo-db merged 17 commits into
mainfrom
air-convert-to-dabs-main

Conversation

@vinchenzo-db

Copy link
Copy Markdown
Contributor

Adds air convert-to-dabs, which translates an AIR CLI run YAML into a deployable Databricks Asset Bundle so a workload authored for air run can be managed and deployed with the standard DABs workflow (validate/deploy/run).

The emitted bundle is schema-valid: the ai_runtime_task maps to the SDK jobs.AiRuntimeTask (experiment + deployments[].{command_path,compute} + code_source_path), with framework fields (retries, timeout, budget policy) on the surrounding task and the runtime environment in environments[].

Snapshotting is owned by the deploy-time aicode mutator, not by convert: code_source_path points at a local directory staged inside the bundle, and bundle deploy (aicode.PackageAndUpload) packages it into a content-addressed tarball and uploads it. convert only lays down the source bytes — copying the working tree (honoring .gitignore) or materializing a pinned git commit into the directory. requirements.yaml is likewise not emitted: aicode.SynthesizeRequirements regenerates it from the environments[] spec, so convert folds the whole dependency set (inline or requirements-file) into that spec instead.

env_variables / secrets / parameters have no native ai_runtime_task field, so they ride as env_vars.json / secret_env_vars.json / hyperparameters.yaml sidecars (same as air run), and a "Notes:" section tells a migrating user what was transformed or staged out-of-band.

This is the top of a 2-PR stack: it builds on the aicode deploy-time packaging mutator so the two compose end-to-end.

Co-authored-by: Isaac

Changes

Why

Tests

@eng-dev-ecosystem-bot

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

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 16219ba

Run: 31132728914

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 4 4 291 1115 3:54
🔄​ aws windows 1 4 4 292 1113 3:56
💚​ azure linux 4 4 290 1115 4:12
💚​ azure windows 4 4 292 1113 4:10
💚​ gcp linux 1 5 291 1115 4:21
💚​ gcp windows 1 5 293 1113 4:13
9 interesting tests: 4 RECOVERED, 4 SKIP, 1 flaky
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
🔄​ TestSyncIncrementalFileOverwritesFolder ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R
Top 3 slowest tests (at least 2 minutes):
duration env testname
4:07 gcp windows TestAccept
3:33 aws windows TestAccept
3:32 azure windows TestAccept

@vinchenzo-db
vinchenzo-db force-pushed the air-convert-to-dabs-main branch 10 times, most recently from e74034f to 29248d4 Compare August 3, 2026 22:03
Base automatically changed from air-code-source-dir-rebase to main August 4, 2026 22:12
Adds `air convert-to-dabs`, which translates an AIR CLI run YAML into a
deployable Databricks Asset Bundle so a workload authored for `air run` can be
managed and deployed with the standard DABs workflow (validate/deploy/run).

The emitted bundle is schema-valid: the ai_runtime_task maps to the SDK
jobs.AiRuntimeTask (experiment + deployments[].{command_path,compute} +
code_source_path), with framework fields (retries, timeout, budget policy) on
the surrounding task and the runtime environment in environments[].

Snapshotting is owned by the deploy-time aicode mutator, not by convert:
code_source_path points at a local *directory* staged inside the bundle, and
`bundle deploy` (aicode.PackageAndUpload) packages it into a content-addressed
tarball and uploads it. convert only lays down the source bytes — copying the
working tree (honoring .gitignore) or materializing a pinned git commit into the
directory. requirements.yaml is likewise not emitted: aicode.SynthesizeRequirements
regenerates it from the environments[] spec, so convert folds the whole dependency
set (inline or requirements-file) into that spec instead.

env_variables / secrets / parameters have no native ai_runtime_task field, so they
ride as env_vars.json / secret_env_vars.json / hyperparameters.yaml sidecars (same
as `air run`), and a "Notes:" section tells a migrating user what was transformed
or staged out-of-band.

This is the top of a 2-PR stack: it builds on the aicode deploy-time packaging
mutator so the two compose end-to-end.

Co-authored-by: Isaac
The Windows CI job failed the convert-to-dabs acceptance test with
"tar (child): Cannot connect to C: resolve failed": the system tar reads the
`C:` in an absolute archive path as a remote host:path.

Two fixes:
- createPlainTarball now passes the archive as a bare basename with cmd.Dir set
  to the output directory (and an absolute parent), so no `C:\...` path reaches
  tar's -f argument. Mirrors how git archive is invoked; safe on GNU tar and
  bsdtar. (This helper is shared with the `air run` snapshot path.)
- extractTarball is rewritten in pure Go (archive/tar + compress/gzip) instead of
  shelling out to `tar -xzf`, eliminating the same drive-letter hazard on the
  extract side and dropping the external-tar dependency for extraction. It rejects
  entries that would escape the destination (path traversal, absolute/escaping
  symlinks) and bounds each file copy to its header size.

Adds unit tests for extractTarball (happy path incl. nested dirs + in-tree
symlink; traversal + escaping-symlink rejection).

Co-authored-by: Isaac
convert-to-dabs is now a purely local, syntactic translation. It no longer copies,
snapshots, or git-archives the code source — the deploy-time aicode mutator (from
the base PR) packages the source in place at `bundle deploy`.

- code_source_path is emitted as the source directory relative to the bundle
  (bundle root defaults to the input YAML's directory, which contains it), rather
  than a copied ./code_source dir.
- Removed the materialize/extract-tarball machinery and the git-pinned-commit
  path. code_source.snapshot.git is now rejected (deploy packages the working
  tree); a code_source outside the bundle directory is rejected with guidance.
- writeBundle now only writes databricks.yml + command.sh + the env/secret/param
  sidecars. Dependencies stay folded into the environments[] spec.

Tests updated: assert code_source_path points at ./src with no copy, reject
git-pin and out-of-bundle sources; the acceptance test converts in place.

Co-authored-by: Isaac
git and remote_volume stay unsupported, but the errors now explain why and what
to do: for git, check out the revision before converting (deploy packages the
working tree as-is); for remote_volume, set workspace.artifact_path (the bundle
artifact location is bundle-wide, not per-code-source).

Co-authored-by: Isaac
Re-running a conversion previously failed with no way to overwrite, and the
suggested remedy (--output-dir) is a dead end in place: code_source must live
inside the bundle dir. Add --force, and point the error at it.

The next-steps message said "databricks bundle ...". A CLI without
ai_runtime_task support only warns on the unknown field and then deploys a job
with no AI task, so print the invoked binary instead and call out the risk.

Co-authored-by: Isaac
The bundle is written next to the input YAML, so `cd .` was a no-op step; it is
now printed only when the bundle lands elsewhere. Drop the notes block and the
stale-CLI warning, and point at `bundle summary` for what was deployed.

Co-authored-by: Isaac
@vinchenzo-db
vinchenzo-db force-pushed the air-convert-to-dabs-main branch from 7a2b086 to c7698c9 Compare August 4, 2026 22:44
Comment thread experimental/air/cmd/convert_to_dabs.go
Comment thread experimental/air/cmd/convert_to_dabs.go Outdated
include_paths narrows the archive to a subset of root_path, which a bundle can't
express per code source — deploy packages the whole directory. Converting was
silently dropping it and uploading files the user meant to exclude, so reject it
and point at sync.exclude / .gitignore.

Emit a job resource key that YAML would type as a non-string scalar ("12345" ->
!!int) as a quoted key instead of prefixing it with "job_". yamlsaver already had
this logic for scalar *values* (isScalarValueInString); apply it to map keys too,
so the resource key keeps the experiment name.

Co-authored-by: Isaac

@ben-hansen-db ben-hansen-db left a comment

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.

Thanks for the updates!

@vinchenzo-db
vinchenzo-db enabled auto-merge August 5, 2026 16:54
vinchenzo-db and others added 3 commits August 5, 2026 12:20
Keep the change inside experimental/air/ instead of teaching the shared
yamlsaver to quote map keys. quoteJobKey rewrites the emitted job resource key
when the experiment name would otherwise load as a non-string scalar ("12345" ->
!!int), which the bundle loader rejects.

NewSaverWithStyle can't do this: its style map applies to a key's whole subtree,
so quoting the job key also quotes every nested key and turns
accelerator_count: 1 into a string.

Co-authored-by: Isaac
It is a verbatim copy of the input YAML, which bundle sync already uploads, and
nothing in the emitted bundle references it.

Co-authored-by: Isaac
@vinchenzo-db
vinchenzo-db disabled auto-merge August 5, 2026 23:48
Write command.sh and the env/secret/param sidecars into generated_artifacts/ and
emit sync.paths listing only that directory. The code directory is no longer
synced as loose files: deploy still packages it into the snapshot tarball, so it
was previously uploaded twice, which is costly for a large source tree.

The server derives the sidecar paths from command_path's parent, so they move
with command.sh.

Co-authored-by: Isaac
The old message suggested --output-dir, which cannot help here: root_path is
resolved relative to the input YAML, so redirecting the output moves the bundle
away from the code rather than enclosing it. sync.paths is the mechanism that
actually widens the sync root.

Co-authored-by: Isaac

@ben-hansen-db ben-hansen-db left a comment

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.

  1. Is this still true? Is experiment-bundle created?
--output-dir string   Directory to write the bundle into (default: a
                      <experiment>-bundle folder next to the input YAML)
  1. Do we do a translation for max_retries? One thing to note is that our default is three so we could set 3 retries as the default in the translation.

  2. Error: code_source root_path "./src" is not inside the bundle directory
    "bundleout"; ... Move the code inside the bundle, or set sync.paths in
    databricks.yml to a directory that contains both (note that widens what
    gets synced). Wouldn't bundle validate reject the sync paths change?

vinchenzo-db and others added 4 commits August 6, 2026 19:02
air run fills its own default of 3 when max_retries is unset, so omitting it
gave the converted bundle the Jobs default instead — the same YAML retried
differently depending on which path launched it.

The --output-dir help still described a <experiment>-bundle folder; the default
is the input YAML's directory.

Co-authored-by: Isaac
Reverts dropping it. The Jobs run-output page derives its workspace path from
command_path (same directory, fixed filename) to show the config a run used, so
omitting the file leaves that link pointing at nothing.

The input YAML cannot serve instead: `air run -f` accepts any path, and real
configs are named train.yaml, workload.yaml, train_kie.yaml and so on, so the UI
has no filename to derive. The duplicate copy at a fixed name is what makes the
path resolvable.

Co-authored-by: Isaac
Co-authored-by: Isaac
…oved)

The main merge (AIR CLI Migration Pt. 2, #6166) stopped emitting a
requirements.yaml sidecar — file-form deps now fold into the environments[]
spec — and deleted the requirementsName constant. convert_to_dabs still
filtered artifacts by it, breaking the build (undefined: requirementsName),
which in turn failed lint, validate-generated, and every acceptance test.

Drop the now-dead filter: buildArtifacts no longer produces requirements.yaml,
so there is nothing to remove. Also update the usage_policy_id test to a UUID
(the merge added UUID validation) and regenerate the engine-matrix snapshot.

Co-authored-by: Isaac
@vinchenzo-db
vinchenzo-db added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 36e6bce Aug 7, 2026
25 checks passed
@vinchenzo-db
vinchenzo-db deleted the air-convert-to-dabs-main branch August 7, 2026 02:02
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 36e6bce

Run: 31139970705

Env ❌​FAIL 🟨​KNOWN 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
❌​ aws linux 6 1 2 5 2 1158 924 103:23
🔄​ azure linux 3 5 2 1058 961 89:10
💚​ gcp linux 3 3 1050 965 93:22
18 interesting tests: 6 FAIL, 5 RECOVERED, 4 flaky, 2 SKIP, 1 KNOWN
Test Name aws linux azure linux gcp linux
🟨​ TestAccept 🟨​K 🔄​f 💚​R
💚​ TestAccept/bundle/invariant/no_drift 💚​R 💚​R 💚​R
🔄​ TestAccept/bundle/resources/clusters/lifecycle-started ✅​p 🔄​f ✅​p
🔄​ TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct ✅​p 🔄​f ✅​p
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions ❌​F 🙈​s 🙈​s
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct ❌​F
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform ❌​F
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions ❌​F 🙈​s 🙈​s
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct ❌​F
❌​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform ❌​F
🔄​ TestAccept/bundle/resources/postgres_synced_tables/recreate 🔄​f 🙈​s 🙈​s
🔄​ TestAccept/bundle/resources/postgres_synced_tables/recreate/DATABRICKS_BUNDLE_ENGINE=terraform 🔄​f
💚​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R
Top 50 slowest tests (at least 2 minutes):
duration env testname
12:36 gcp linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
12:04 aws linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
8:05 aws linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:23 gcp linux TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:14 aws linux TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
6:53 gcp linux TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DATABRICKS_BUNDLE_ENGINE=direct
6:38 azure linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
6:37 gcp linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
6:27 gcp linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
5:54 azure linux TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
5:53 aws linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
5:18 azure linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
5:18 gcp linux TestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
4:58 gcp linux TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
4:54 azure linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
4:43 gcp linux TestAccept/bundle/resources/apps/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
4:39 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
4:31 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
4:03 aws linux TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
3:54 gcp linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
3:43 gcp linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
3:35 azure linux TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
3:30 aws linux TestAccept/bundle/resources/apps/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
3:09 aws linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
3:06 gcp linux TestAccept/bundle/resources/clusters/resize-terminated-fallback/DATABRICKS_BUNDLE_ENGINE=direct
3:04 aws linux TestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
3:00 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:57 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:56 aws linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
2:51 azure linux TestAccept/bundle/resources/apps/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
2:50 gcp linux TestAccept
2:46 gcp linux TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=terraform
2:45 azure linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
2:44 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:42 azure linux TestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
2:39 gcp linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
2:38 gcp linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
2:37 gcp linux TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
2:31 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:27 gcp linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
2:26 gcp linux TestAccept/bundle/resources/clusters/deploy/data_security_mode/DATABRICKS_BUNDLE_ENGINE=direct
2:26 azure linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
2:25 gcp linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
2:25 gcp linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
2:25 gcp linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
2:23 gcp linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
2:17 gcp linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
2:13 gcp linux TestAccept/bundle/resources/clusters/deploy/simple/DATABRICKS_BUNDLE_ENGINE=direct
2:09 gcp linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
2:08 gcp linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=

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.

3 participants