Skip to content
Draft
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 @@ -5,6 +5,7 @@
### Notable Changes

### CLI
* `workspace export-dir` no longer aborts when a workspace object's name is not a legal local filename (e.g. a notebook named `New Notebook 2026-05-04 13:54:24` whose `:` is illegal on Windows). Such files are now skipped with a warning and the export completes ([#5171](https://github.com/databricks/cli/issues/5171)).

### Bundles
* `bundle run` now prints the modern job run URL (`/jobs/<id>/runs/<id>`) so that non-admin users permitted to view the run are taken to the run instead of the workspace homepage.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Databricks notebook source
print("hello")

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
@@ -0,0 +1,5 @@

>>> [CLI] workspace export-dir /test-dir [TEST_TMP_DIR]/export
Exporting files from /test-dir
/test-dir/New Notebook [TIMESTAMP] (skipped; invalid name for local file system)
Export complete
7 changes: 7 additions & 0 deletions acceptance/cmd/workspace/export-dir-illegal-filename/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$CLI workspace import "/test-dir/New Notebook 2026-05-04 13:54:24.py" --file notebook.py --format AUTO --language PYTHON

mkdir -p "$TEST_TMP_DIR/export"

# On Windows the notebook name contains ':' which is illegal in a local filename.
# export-dir should skip it with a warning and still complete (databricks/cli#5171).
trace $CLI workspace export-dir /test-dir "$TEST_TMP_DIR/export"
28 changes: 28 additions & 0 deletions acceptance/cmd/workspace/export-dir-illegal-filename/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Local = true
Cloud = false

# This reproduces databricks/cli#5171, which only manifests on Windows: a ':' in
# the notebook name is a legal filename character on Linux/macOS but illegal on
# Windows, so os.Create rejects the local target path there.
[GOOS]
darwin = false
linux = false
windows = true

[Env]
MSYS_NO_PATHCONV = "1"

[[Server]]
Pattern = "GET /api/2.0/workspace/list"
Response.Body = '''
{
"objects": [
{
"path": "/test-dir/New Notebook 2026-05-04 13:54:24",
"object_type": "NOTEBOOK",
"language": "PYTHON",
"object_id": 123
}
]
}
'''
7 changes: 7 additions & 0 deletions cmd/workspace/workspace/export_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ func (opts *exportDirOptions) callback(ctx context.Context, workspaceFiler filer
// create the file
f, err := os.Create(targetPath)
if err != nil {
// A workspace name can be illegal as a local filename (e.g. a ':'
// in "New Notebook 2026-05-04 13:54:24" on Windows). Skip those with
// a warning rather than aborting the whole export (#5171).
if isInvalidLocalNameError(err) {
cmdio.LogString(ctx, sourcePath+" (skipped; invalid name for local file system)")
return nil
}
return err
}
defer f.Close()
Expand Down
11 changes: 11 additions & 0 deletions cmd/workspace/workspace/export_dir_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !windows

package workspace

// isInvalidLocalNameError reports whether err means the workspace object could
// not be written because its name is not a legal filename on the local OS. On
// non-Windows platforms the only bytes illegal in a filename are '/' and NUL,
// neither of which can appear in a workspace object name, so this never fires.
func isInvalidLocalNameError(err error) bool {
return false
}
21 changes: 21 additions & 0 deletions cmd/workspace/workspace/export_dir_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build windows

package workspace

import (
"errors"
"syscall"
)

// errorInvalidName is the Windows ERROR_INVALID_NAME code. The file APIs return
// it when a path contains characters that are illegal in a local filename, such
// as the ':' in a notebook named "New Notebook 2026-05-04 13:54:24". It is not
// declared in the standard syscall package, so we use the well-known code.
// https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
const errorInvalidName = syscall.Errno(0x7b)

// isInvalidLocalNameError reports whether err means the workspace object could
// not be written because its name is not a legal filename on the local OS.
func isInvalidLocalNameError(err error) bool {
return errors.Is(err, errorInvalidName)
}
56 changes: 56 additions & 0 deletions cmd/workspace/workspace/export_dir_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//go:build windows

package workspace

import (
"errors"
"io/fs"
"os"
"syscall"
"testing"

"github.com/stretchr/testify/assert"
)

// The narrow contract: only the Windows "invalid file name" error is treated as
// skippable. Genuine failures (permission, missing path, anything else) must not
// be swallowed, otherwise export-dir would silently drop files on real errors.
func TestIsInvalidLocalNameError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "invalid name wrapped in PathError",
err: &os.PathError{Op: "open", Path: `C:\tmp\New Notebook 13:54:24.py`, Err: syscall.Errno(0x7b)},
want: true,
},
{
name: "permission denied is not skipped",
err: fs.ErrPermission,
want: false,
},
{
name: "not exist is not skipped",
err: fs.ErrNotExist,
want: false,
},
{
name: "generic error is not skipped",
err: errors.New("boom"),
want: false,
},
{
name: "nil is not skipped",
err: nil,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isInvalidLocalNameError(tt.err))
})
}
}
Loading