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 .nextchanges/air/7639.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `air run` now rejects a `code_source.include_paths` entry that matches no files instead of uploading a snapshot without it. ([#7639](https://github.com/databricks/cli/pull/7639))
31 changes: 31 additions & 0 deletions cmd/air/snapshot_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"

Expand Down Expand Up @@ -193,9 +194,39 @@ func snapshotFiles(ctx context.Context, repoPath string, includePaths []string,
if len(files) == 0 {
return nil, emptySnapshotError(ctx, repoPath, includePaths, isGitRepo)
}
if unmatched := unmatchedIncludePaths(includePaths, files); len(unmatched) > 0 {
return nil, fmt.Errorf("code_source include_paths select no files under %s: %s. Check that each path exists under root_path and is not ignored by git (`git check-ignore -v <path>`)", filepath.ToSlash(repoPath), strings.Join(unmatched, ", "))
}
return files, nil
}

// unmatchedIncludePaths returns the include_paths entries that select none of files.
// git ls-files ignores a pathspec that matches nothing, so a mistyped entry would be
// left out of the snapshot and the run would fail on the node after compute was
// provisioned. Entries with glob characters are skipped because git matches them as
// patterns rather than as path prefixes.
func unmatchedIncludePaths(includePaths []string, files []snapshotFile) []string {
var unmatched []string
for _, p := range includePaths {
if strings.ContainsAny(p, "*?[") {
continue
}
prefix := path.Clean(filepath.ToSlash(p))
matched := prefix == "."
for _, file := range files {
rel := filepath.ToSlash(file.rel)
if rel == prefix || strings.HasPrefix(rel, prefix+"/") {
matched = true
break
}
}
if !matched {
unmatched = append(unmatched, p)
}
}
return unmatched
}

// emptySnapshotError explains why no file was selected for the snapshot. An empty
// tarball is accepted by the upload but fails on the node, after compute was
// provisioned, so it is rejected here with the most likely cause.
Expand Down
39 changes: 39 additions & 0 deletions cmd/air/snapshot_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,42 @@ func TestSnapshotFiles_RejectsIncludePathsMatchingNothing(t *testing.T) {
assert.Contains(t, err.Error(), "contains no files")
assert.Contains(t, err.Error(), "include_paths")
}

func TestSnapshotFiles_RejectsIncludePathMatchingNothingAlongsideOthers(t *testing.T) {
repo := t.TempDir()
writeRepoFile(t, repo, "ray_bootstrap.sh", "#!/bin/bash\n")
writeRepoFile(t, repo, "my-job/main.py", "print(1)\n")

_, err := snapshotFiles(t.Context(), repo, []string{"ray_bootstrap.sh", "my_job"}, false)
require.Error(t, err)
assert.Contains(t, err.Error(), "include_paths select no files")
assert.Contains(t, err.Error(), ": my_job.")
assert.NotContains(t, err.Error(), "ray_bootstrap.sh")
}

func TestSnapshotFiles_RejectsIncludePathWhoseFilesAreIgnored(t *testing.T) {
repo := newTestRepo(t)
writeRepoFile(t, repo, ".gitignore", "build/\n")
writeRepoFile(t, repo, "main.py", "print(1)\n")
writeRepoFile(t, repo, "build/out.bin", "x")

_, err := snapshotFiles(t.Context(), repo, []string{"main.py", "build"}, true)
require.Error(t, err)
assert.Contains(t, err.Error(), ": build.")
assert.Contains(t, err.Error(), "check-ignore -v")
}

func TestSnapshotFiles_AcceptsIncludePathForms(t *testing.T) {
repo := t.TempDir()
writeRepoFile(t, repo, "src/model.py", "print()")
writeRepoFile(t, repo, "README.md", "readme")
writeRepoFile(t, repo, "notes.txt", "notes")

files, err := snapshotFiles(t.Context(), repo, []string{"./src/", "README.md", "*.txt"}, false)
require.NoError(t, err)
var rels []string
for _, file := range files {
rels = append(rels, filepath.ToSlash(file.rel))
}
assert.ElementsMatch(t, []string{"src/model.py", "README.md", "notes.txt"}, rels)
}
Loading