Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 0 additions & 28 deletions acceptance/bundle/deploy/readplan/whl-not-uploaded/script

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
bundle:
name: whl-not-uploaded
name: whl-upload

resources:
jobs:
my_job:
name: whl-not-uploaded-job
name: whl-upload-job
tasks:
- task_key: main
new_cluster:
Expand All @@ -15,6 +15,5 @@ resources:
package_name: my_test_code
entry_point: run
libraries:
# Local wheel, prebuilt (no build: step) to keep the test hermetic. See script
# for the bug this exercises. Behaves the same as a wheel with a build: step.
# Local wheel, prebuilt (no build: step) to keep the test hermetic.
- whl: ./dist/*.whl
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@
"plan_version": [PLAN_VERSION],
"cli_version": "[CLI_VERSION]",
"lineage": "[UUID]",
"serial": 2,
"plan": {
"resources.jobs.my_job": {
"action": "create",
"new_state": {
"value": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/whl-not-uploaded/default/state/metadata.json"
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/whl-upload/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "whl-not-uploaded-job",
"name": "whl-upload-job",
"queue": {
"enabled": true
},
"tasks": [
{
"libraries": [
{
"whl": "/Workspace/Users/[USERNAME]/.bundle/whl-not-uploaded/default/artifacts/.internal/my_test_code-0.0.1-py3-none-any.whl"
"whl": "/Workspace/Users/[USERNAME]/.bundle/whl-upload/default/artifacts/.internal/my_test_code-0.0.1-py3-none-any.whl"
}
],
"new_cluster": {
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
=== bundle deploy uploads the wheel
>>> [CLI] bundle deploy
Uploading dist/my_test_code-0.0.1-py3-none-any.whl...
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/whl-not-uploaded/default/files...
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/whl-upload/default/files...
Created jobs.my_job
Files: 4 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged

>>> print_requests.py //artifacts/.internal --del-field raw_body
{
"method": "POST",
"path": "/api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/whl-not-uploaded/default/artifacts/.internal/my_test_code-0.0.1-py3-none-any.whl",
"path": "/api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/whl-upload/default/artifacts/.internal/my_test_code-0.0.1-py3-none-any.whl",
"q": {
"overwrite": "true"
}
Expand All @@ -20,25 +20,31 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged
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/whl-not-uploaded/default
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/whl-upload/default

Destroy: 1 deleted

=== bundle deploy --plan does NOT upload the wheel (bug)
>>> [CLI] bundle plan -o json

=== bundle deploy --plan also uploads the wheel
>>> [CLI] bundle deploy --plan tmp.plan.json
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/whl-not-uploaded/default/files...
Uploading dist/my_test_code-0.0.1-py3-none-any.whl...
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/whl-upload/default/files...
Created jobs.my_job
Files: 6 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged

>>> print_requests.py //artifacts/.internal --del-field raw_body
{
"method": "POST",
"path": "/api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/whl-upload/default/artifacts/.internal/my_test_code-0.0.1-py3-none-any.whl",
"q": {
"overwrite": "true"
}
}

>>> [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/whl-not-uploaded/default
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/whl-upload/default

Destroy: 1 deleted
27 changes: 27 additions & 0 deletions acceptance/bundle/deploy/readplan/whl-upload/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# A local wheel referenced by a job library must be uploaded to its .../artifacts/.internal/
# workspace path by both `bundle deploy` and `bundle deploy --plan`. Applying a saved plan
# runs phases.FindLibraries to discover which local files to upload without re-running any
# build commands. This test pins that both deploy paths upload the wheel identically.

title "bundle deploy uploads the wheel"
trace $CLI bundle deploy
# An import-file request uploaded the wheel to its .../artifacts/.internal/ workspace path.
# raw_body is the wheel's binary payload; drop it to keep the golden readable.
trace print_requests.py //artifacts/.internal --del-field raw_body

trace $CLI bundle destroy --auto-approve
# Discard destroy's requests so the saved-plan deploy below starts from a clean recording.
rm "$OUT_REQUESTS"

title "bundle deploy --plan also uploads the wheel"
# tmp.plan.json is the live plan (with features/serial for validation);
# out.plan.json is the nostamp-stripped golden for cross-variant comparison.
$CLI bundle plan -o json > tmp.plan.json
$CLI bundle plan -o json | nostamp > out.plan.json
trace $CLI bundle deploy --plan tmp.plan.json
# The same import-file request uploads the wheel to the .../artifacts/.internal/ path the
# job task points at, matching the plain `bundle deploy` above.
trace print_requests.py //artifacts/.internal --del-field raw_body

trace $CLI bundle destroy --auto-approve
rm "$OUT_REQUESTS"
6 changes: 6 additions & 0 deletions acceptance/bundle/deploy/readplan/whl-upload/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# --plan is direct-engine only.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
# Saved plans don't carry the deployment stamp, so this test skips the DMS variant.
EnvMatrix.DMS = [""]
RecordRequests = true
Ignore = [".databricks", "tmp.plan.json"]
23 changes: 23 additions & 0 deletions bundle/artifacts/expand_globs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package artifacts
import (
"context"
"fmt"
"maps"
"path/filepath"
"slices"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
Expand Down Expand Up @@ -122,3 +124,24 @@ func (e expandGlobs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnosti

return diags
}

type expandAllGlobs struct{}

func (e *expandAllGlobs) Name() string {
return "artifacts.ExpandGlobReferences"
}

func (e *expandAllGlobs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
for _, name := range slices.Sorted(maps.Keys(b.Config.Artifacts)) {
bundle.ApplyContext(ctx, b, expandGlobs{name: name})
}
return nil
}

// ExpandGlobReferences returns a mutator that expands glob patterns in artifact
// file sources for every artifact. Prepare handles artifacts without a build
// command; Build handles those with one. This mutator covers both, making it
// safe to call after Prepare but before a build has run (e.g. FindLibraries).
func ExpandGlobReferences() bundle.Mutator {
return &expandAllGlobs{}
}
56 changes: 45 additions & 11 deletions bundle/phases/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,36 @@ import (
// Computed by Build and consumed by Deploy to upload the right files.
type LibLocationMap map[string][]libraries.LocationToUpdate

// Build runs the build phase, which builds artifacts.
func Build(ctx context.Context, b *bundle.Bundle) LibLocationMap {
log.Info(ctx, "Phase: build")

bundle.ApplySeqContext(ctx, b,
scripts.Execute(config.ScriptPreBuild),
artifacts.Build(),
scripts.Execute(config.ScriptPostBuild),

// resolveLibraries runs variable resolution, glob expansion, path rewriting,
// and wheel-task transformation to produce the local→remote upload map.
// extra mutators are applied after CheckForSameNameLibraries and before
// ReplaceWithRemotePath; Build passes libraries.SwitchToPatchedWheels() there.
func resolveLibraries(ctx context.Context, b *bundle.Bundle, extra ...bundle.Mutator) LibLocationMap {
mutators := make([]bundle.Mutator, 0, 4+len(extra))
mutators = append(mutators,
mutator.ResolveVariableReferencesWithoutResources(
"artifacts",
),
mutator.ResolveVariableReferencesOnlyResources(
"artifacts",
),

// Expand artifact file globs (e.g. dist/*.whl in artifacts[*].files[*].source).
// Prepare() skips this for artifacts with a build command because the files don't
// exist yet; Build() does it after running the command. For FindLibraries (plan
// apply) the artifacts were already built at plan time, so we expand here instead.
// Safe to call from Build too: already-expanded paths are left unchanged.
artifacts.ExpandGlobReferences(),

// libraries.CheckForSameNameLibraries() needs to be run after we expand glob references so we
// know what are the actual library paths.
// libraries.ExpandGlobReferences() has to be run after the libraries are built and thus this
// mutator is part of the deploy step rather than validate.
libraries.ExpandGlobReferences(),
libraries.CheckForSameNameLibraries(),
// SwitchToPatchedWheels must be run after ExpandGlobReferences and after build phase because it Artifact.Source and Artifact.Patched populated
libraries.SwitchToPatchedWheels(),
)
mutators = append(mutators, extra...)
bundle.ApplySeqContext(ctx, b, mutators...)

if logdiag.HasError(ctx) {
return nil
Expand All @@ -55,3 +60,32 @@ func Build(ctx context.Context, b *bundle.Bundle) LibLocationMap {
bundle.ApplyContext(ctx, b, trampoline.TransformWheelTask())
return libs
}

// Build runs the build phase, which builds artifacts.
func Build(ctx context.Context, b *bundle.Bundle) LibLocationMap {
log.Info(ctx, "Phase: build")

bundle.ApplySeqContext(ctx, b,
scripts.Execute(config.ScriptPreBuild),
artifacts.Build(),
scripts.Execute(config.ScriptPostBuild),
)

if logdiag.HasError(ctx) {
return nil
}

// SwitchToPatchedWheels must be passed to resolveLibraries so it runs after
// ExpandGlobReferences (which expands *.whl patterns in job library paths)
// and after the build phase (which populates Artifact.Source and Artifact.Patched).
return resolveLibraries(ctx, b, libraries.SwitchToPatchedWheels())
}

// FindLibraries discovers which local library files need uploading by reading
// the bundle config (glob expansion and path rewriting) without running any
// build commands. Used when applying a saved plan: artifacts were already built
// at plan time and the plan's new_state carries the correct remote paths.
func FindLibraries(ctx context.Context, b *bundle.Bundle) LibLocationMap {
log.Info(ctx, "Phase: find libraries")
return resolveLibraries(ctx, b)
}
25 changes: 23 additions & 2 deletions cmd/bundle/utils/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ type ProcessOptions struct {
Deploy bool

// Path to pre-computed plan JSON file (direct engine only).
// When set, skips Build and PreDeployChecks phases, loads plan from file instead of calculating.
// When set, skips build commands and the PreDeployChecks phase, and loads the
// plan from the file instead of calculating it. Library file discovery runs
// (via FindLibraries) so that the correct local files are found and uploaded
// to the remote paths the plan already references.
ReadPlanPath string

// PostStateFunc is called at the end of ProcessBundleRet, within the state lifecycle scope
Expand Down Expand Up @@ -352,14 +355,21 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle
}

var plan *deployplan.Plan

var findLibraries bool
if opts.ReadPlanPath != "" {
if !stateDesc.Engine.IsDirect() {
logdiag.LogError(ctx, errors.New("--plan is only supported with direct engine (set bundle.engine to \"direct\" or DATABRICKS_BUNDLE_ENGINE=direct)"))
return b, stateDesc, root.ErrAlreadyPrinted
}
// Build commands are not re-run when applying a saved plan: re-running them
// could produce new artifact files with a different mtime which, with
// dynamic_version, would yield a different patched filename than the remote
// path the plan already has baked in. Library discovery still runs (via
// FindLibraries) so the correct local files are found and uploaded to the
// remote paths the plan references.
opts.Build = false
opts.PreDeployChecks = false
findLibraries = true

var err error
plan, err = deployplan.LoadPlanFromFile(opts.ReadPlanPath)
Expand Down Expand Up @@ -419,6 +429,17 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle
Value: time.Since(t2).Milliseconds(),
})

if logdiag.HasError(ctx) {
return b, stateDesc, root.ErrAlreadyPrinted
}
} else if findLibraries {
t2 := time.Now()
libs = phases.FindLibraries(ctx, b)
b.Metrics.ExecutionTimes = append(b.Metrics.ExecutionTimes, protos.IntMapEntry{
Key: "phases.FindLibraries",
Value: time.Since(t2).Milliseconds(),
})

if logdiag.HasError(ctx) {
return b, stateDesc, root.ErrAlreadyPrinted
}
Expand Down
Loading