Skip to content

Add --dry-run flag to sync and bundle-sync command #2657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 16, 2025
Merged
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
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Dependency updates

### CLI
* Added dry-run flag support to sync command ([#2657](https://github.com/databricks/cli/pull/2657))

### Bundles
* Do not exit early when checking incompatible tasks for specified DBR ([#2692](https://github.com/databricks/cli/pull/2692))
Expand Down
1 change: 1 addition & 0 deletions acceptance/bundle/help/bundle-sync/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Usage:
databricks bundle sync [flags]

Flags:
--dry-run simulate sync execution without making actual changes
--full perform full synchronization (default is incremental)
-h, --help help for sync
--interval duration file system polling interval (for --watch) (default 1s)
Expand Down
7 changes: 7 additions & 0 deletions acceptance/bundle/sync/dryrun/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle:
name: bundle-sync-test

resources:
dashboards:
dashboard1:
display_name: My dashboard
10 changes: 10 additions & 0 deletions acceptance/bundle/sync/dryrun/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

>>> [CLI] bundle sync --dry-run --output text
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded databricks.yml
Uploaded project-folder
Uploaded project-folder/app.py
Uploaded project-folder/app.yaml
Uploaded project-folder/query.sql
18 changes: 18 additions & 0 deletions acceptance/bundle/sync/dryrun/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
mkdir "project-folder" "ignored-folder" "ignored-folder/folder1"
touch "project-folder/app.yaml" "project-folder/app.py" "project-folder/query.sql"
touch "ignored-folder/script.py" "ignored-folder/folder1/script.py"
cat > .gitignore << EOF
ignored-folder/
script
output.txt
repls.json
EOF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be more natural to prepare this structure and commit it to git?

You would need to come up with another name for .gitignore (e.g. test-gitignore) and do 'mv test-gitignore .gitignore' before your test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that method did not occur to me. That is probably worth adding to the acceptance testing contribution guidelines.


cleanup() {
rm .gitignore
rm -rf project-folder ignored-folder .git .databricks
}
trap cleanup EXIT

# Note: output line starting with "Action: " lists files in non-deterministic order so we filter it out
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're sorting anyway, so you already handle non-deterministic order, right?

trace $CLI bundle sync --dry-run --output text | grep -v "^Action: " | sort

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorting output lines, but "Action:" is listing all files affected by the operation in a random order in a single line. I figured the easiest way around is to remove it from the output altogether

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we try get rid of non-determinism in output (where feasible).

Is this due to map being used to hold the files? In that case I'd either get rid of map or use SortedKeys helper before printing.

trace $CLI bundle sync --dry-run --output text | grep -v "^Action: " | sort
2 changes: 2 additions & 0 deletions acceptance/bundle/sync/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Initial Sync Complete
Uploaded .gitignore
Uploaded databricks.yml
Uploaded dryrun
Uploaded dryrun/databricks.yml
Uploaded ignored-folder/folder1
Uploaded ignored-folder/folder1/script.py
Uploaded ignored-folder/script.py
49 changes: 49 additions & 0 deletions acceptance/cmd/sync/dryrun/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

>>> [CLI] sync . /Users/[USERNAME] --dry-run
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded project-folder
Uploaded project-folder/app.py
Uploaded project-folder/app.yaml
Uploaded project-folder/query.sql

>>> [CLI] sync . /Users/[USERNAME] --dry-run --exclude project-folder/app.*
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded project-folder
Uploaded project-folder/query.sql

>>> [CLI] sync . /Users/[USERNAME] --dry-run --exclude project-folder/app.* --exclude project-folder/query.sql
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore

>>> [CLI] sync . /Users/[USERNAME] --dry-run --exclude project-folder/app.* --exclude project-folder/query.sql --include ignored-folder/*.py
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded ignored-folder
Uploaded ignored-folder/script.py

>>> [CLI] sync . /Users/[USERNAME] --dry-run --exclude project-folder/app.* --exclude project-folder/query.sql --include ignored-folder/**/*.py
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded ignored-folder/folder1
Uploaded ignored-folder/folder1/script.py
Uploaded ignored-folder/script.py

>>> [CLI] sync . /Users/[USERNAME] --dry-run --include ignored-folder/** --include !ignored-folder/folder1/big-blob
Warn: Running in dry-run mode. No actual changes will be made.
Initial Sync Complete
Uploaded .gitignore
Uploaded ignored-folder/folder1
Uploaded ignored-folder/folder1/script.py
Uploaded ignored-folder/folder1/script.yaml
Uploaded ignored-folder/script.py
Uploaded project-folder
Uploaded project-folder/app.py
Uploaded project-folder/app.yaml
Uploaded project-folder/query.sql
33 changes: 33 additions & 0 deletions acceptance/cmd/sync/dryrun/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mkdir "project-folder" "ignored-folder" "ignored-folder/folder1" ".git"
touch "project-folder/app.yaml" "project-folder/app.py" "project-folder/query.sql"
touch "ignored-folder/script.py" "ignored-folder/folder1/script.py" "ignored-folder/folder1/script.yaml" "ignored-folder/folder1/big-blob"
cat > .gitignore << EOF
ignored-folder/
script
output.txt
repls.json
EOF

cleanup() {
rm .gitignore
rm -rf project-folder ignored-folder .git
}
trap cleanup EXIT

# Note: output line starting with Action lists files in non-deterministic order so we filter it out
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run | grep -v "^Action" | sort

# excluding by mask:
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run --exclude 'project-folder/app.*' | grep -v "^Action" | sort

# combining excludes:
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run --exclude 'project-folder/app.*' --exclude 'project-folder/query.sql' | grep -v "^Action" | sort

# combining excludes and includes:
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run --exclude 'project-folder/app.*' --exclude 'project-folder/query.sql' --include 'ignored-folder/*.py' | grep -v "^Action" | sort

# include sub-folders:
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run --exclude 'project-folder/app.*' --exclude 'project-folder/query.sql' --include 'ignored-folder/**/*.py' | grep -v "^Action" | sort

# use negated include to exclude files from syncing:
trace $CLI sync . /Users/$CURRENT_USER_NAME --dry-run --include 'ignored-folder/**' --include '!ignored-folder/folder1/big-blob' | grep -v "^Action" | sort
1 change: 1 addition & 0 deletions acceptance/cmd/sync/script
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repls.json
EOF

cleanup() {
rm .gitignore
rm -rf project-folder ignored-folder .git
}
trap cleanup EXIT
Expand Down
7 changes: 7 additions & 0 deletions cmd/bundle/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type syncFlags struct {
full bool
watch bool
output flags.Output
dryRun bool
}

func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, b *bundle.Bundle) (*sync.SyncOptions, error) {
Expand All @@ -47,6 +48,7 @@ func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, b *bundle.Bundle)

opts.Full = f.full
opts.PollInterval = f.interval
opts.DryRun = f.dryRun
return opts, nil
}

Expand All @@ -62,6 +64,7 @@ func newSyncCommand() *cobra.Command {
cmd.Flags().BoolVar(&f.full, "full", false, "perform full synchronization (default is incremental)")
cmd.Flags().BoolVar(&f.watch, "watch", false, "watch local file system for changes")
cmd.Flags().Var(&f.output, "output", "type of the output format")
cmd.Flags().BoolVar(&f.dryRun, "dry-run", false, "simulate sync execution without making actual changes")

cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
Expand Down Expand Up @@ -89,6 +92,10 @@ func newSyncCommand() *cobra.Command {

log.Infof(ctx, "Remote file sync location: %v", opts.RemotePath)

if opts.DryRun {
log.Warnf(ctx, "Running in dry-run mode. No actual changes will be made.")
}

if f.watch {
return s.RunContinuous(ctx)
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type syncFlags struct {
output flags.Output
exclude []string
include []string
dryRun bool
}

func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, args []string, b *bundle.Bundle) (*sync.SyncOptions, error) {
Expand All @@ -46,6 +47,7 @@ func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, args []string, b *
opts.WorktreeRoot = b.WorktreeRoot
opts.Exclude = append(opts.Exclude, f.exclude...)
opts.Include = append(opts.Include, f.include...)
opts.DryRun = f.dryRun
return opts, nil
}

Expand All @@ -72,6 +74,10 @@ func (f *syncFlags) syncOptionsFromArgs(cmd *cobra.Command, args []string) (*syn
ctx := cmd.Context()
client := cmdctx.WorkspaceClient(ctx)

if f.dryRun {
log.Warnf(ctx, "Running in dry-run mode. No actual changes will be made.")
}

localRoot := vfs.MustNew(args[0])
info, err := git.FetchRepositoryInfo(ctx, localRoot.Native(), client)
if err != nil {
Expand Down Expand Up @@ -105,6 +111,7 @@ func (f *syncFlags) syncOptionsFromArgs(cmd *cobra.Command, args []string) (*syn
WorkspaceClient: client,

OutputHandler: outputHandler,
DryRun: f.dryRun,
}
return &opts, nil
}
Expand All @@ -126,6 +133,7 @@ func New() *cobra.Command {
cmd.Flags().Var(&f.output, "output", "type of output format")
cmd.Flags().StringSliceVar(&f.exclude, "exclude", nil, "patterns to exclude from sync (can be specified multiple times)")
cmd.Flags().StringSliceVar(&f.include, "include", nil, "patterns to include in sync (can be specified multiple times)")
cmd.Flags().BoolVar(&f.dryRun, "dry-run", false, "simulate sync execution without making actual changes")

// Wrapper for [root.MustWorkspaceClient] that disables loading authentication configuration from a bundle.
mustWorkspaceClient := func(cmd *cobra.Command, args []string) error {
Expand Down
16 changes: 9 additions & 7 deletions libs/sync/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ type EventBase struct {
Timestamp time.Time `json:"timestamp"`
Seq int `json:"seq"`
Type EventType `json:"type"`
DryRun bool `json:"dry_run,omitempty"`
}

func newEventBase(seq int, typ EventType) *EventBase {
func newEventBase(seq int, typ EventType, dryRun bool) *EventBase {
return &EventBase{
Timestamp: time.Now(),
Seq: seq,
Type: typ,
DryRun: dryRun,
}
}

Expand Down Expand Up @@ -73,9 +75,9 @@ func (e *EventStart) String() string {
return "Action: " + e.EventChanges.String()
}

func newEventStart(seq int, put, delete []string) Event {
func newEventStart(seq int, put, delete []string, dryRun bool) Event {
return &EventStart{
EventBase: newEventBase(seq, EventTypeStart),
EventBase: newEventBase(seq, EventTypeStart, dryRun),
EventChanges: &EventChanges{Put: put, Delete: delete},
}
}
Expand Down Expand Up @@ -106,9 +108,9 @@ func (e *EventSyncProgress) String() string {
}
}

func newEventProgress(seq int, action EventAction, path string, progress float32) Event {
func newEventProgress(seq int, action EventAction, path string, progress float32, dryRun bool) Event {
return &EventSyncProgress{
EventBase: newEventBase(seq, EventTypeProgress),
EventBase: newEventBase(seq, EventTypeProgress, dryRun),

Action: action,
Path: path,
Expand All @@ -133,9 +135,9 @@ func (e *EventSyncComplete) String() string {
return "Complete"
}

func newEventComplete(seq int, put, delete []string) Event {
func newEventComplete(seq int, put, delete []string, dryRun bool) Event {
return &EventSyncComplete{
EventBase: newEventBase(seq, EventTypeComplete),
EventBase: newEventBase(seq, EventTypeComplete, dryRun),
EventChanges: &EventChanges{Put: put, Delete: delete},
}
}
Expand Down
Loading
Loading