From 0614ff38002eae2154fa728dd219f4d90c639a28 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 8 Sep 2026 15:20:21 +0200 Subject: [PATCH 1/3] acc: Extract bundle cleanup in a separate test --- acceptance/acceptance_test.go | 58 +++++++++++++++++-------------- acceptance/bundle_clean_test.go | 60 +++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 281fa699f6e..41399588958 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -459,13 +459,12 @@ func testAccept(t *testing.T, inprocessMode bool, selectedTests []string, skipTo t.Setenv("NODE_TYPE_ID", nodeTypeID) repls.Set(nodeTypeID, "[NODE_TYPE_ID]") - // On cloud, tag every $UNIQUE_NAME with a per-process prefix (see - // newBundleNamePrefix) so this leg's bundles can be attributed and swept, and - // destroy them once all tests finish. Registered before the tests are spawned - // so it runs after they complete. Off cloud names need no attribution. + // On cloud, tag every $UNIQUE_NAME with a per-run prefix (see newBundleNamePrefix) + // so this run's bundles can be attributed and swept by the post-run cleanup step + // (TestCleanupLeakedBundles) after every matrix leg finishes. Off cloud names need + // no attribution: each local test gets a throwaway in-memory fake workspace. if cloudEnv != "" { bundleNamePrefix = newBundleNamePrefix() - setupBundleCleanup(t, execPath, bundleNamePrefix) } testDirs := getTests(t) @@ -718,14 +717,14 @@ func getSkipReason(config *internal.TestConfig, configPath string) string { // Cap at 11 digits: the prefix "cix" plus the 8-char random // minimum must fit the 26-char unique name (26 - 8 - len("ci")-len("x") - -// bundleLegSuffixLen = 11), so a longer GITHUB_RUN_ID falls through to a random -// id rather than building a prefix ciUniqueName would silently drop. +// bundleLegSuffixLen = 11), so a longer GITHUB_RUN_ID is treated as absent +// rather than building a prefix ciUniqueName would silently drop. var ciRunID = regexp.MustCompile(`^[0-9]{1,11}$`) -// bundleLegSuffixLen is the length of the per-process random suffix. 36^4 values -// keep an accidental collision between the few matrix legs that share a workspace -// within one run (which would let one leg destroy another's live bundles) -// negligible, while still leaving >=8 random characters after an 11-digit run id. +// bundleLegSuffixLen is the length of the per-process random suffix that keeps +// each matrix leg's bundle names distinct within a run (useful when eyeballing +// leaked deployments). The run-wide cleanup matches on the "cix" prefix +// alone, so it sweeps every leg regardless of the suffix. const bundleLegSuffixLen = 4 // bundleNamePrefix is the sweepable prefix embedded into every $UNIQUE_NAME on @@ -733,28 +732,37 @@ const bundleLegSuffixLen = 4 // empty off cloud where names need no attribution. var bundleNamePrefix string -// newBundleNamePrefix builds the "cix" prefix that attributes -// deployed bundles to this test process so cleanup can sweep them. -// -// runID is the GitHub run id, or a random numeric id when it is unset/malformed -// (e.g. a local `deco env run`). All matrix legs of a CI run share one GitHub run -// id and legs of different OSes share a workspace, so a run-id-only prefix would -// let one leg's cleanup destroy another leg's live bundles; the random -// lowercase-alphanumeric suffix (bundleLegSuffixLen chars) makes each leg's prefix -// distinct while keeping "cix" a matchable substring for the run-wide -// sweeper (sweep_test_resources.py). The run id (all digits) is delimited by "x" -// so that prefix stays collision-free between runs whose ids share a prefix. -func newBundleNamePrefix() string { +// ciRunPrefix returns the run-wide "cix" prefix that attributes every +// bundle a cloud run deploys to its GitHub run, so they can be swept by +// TestCleanupLeakedBundles and tools/sweep_test_resources.py. The run id (all +// digits) is delimited by "x" so the prefix stays collision-free between runs +// whose ids share a leading substring. Returns "" when GITHUB_RUN_ID is unset or +// not a valid numeric id (e.g. a local `deco env run`), where there is no shared +// run id to key attribution on. +func ciRunPrefix() string { runID := os.Getenv("GITHUB_RUN_ID") if !ciRunID.MatchString(runID) { - runID = strconv.Itoa(rand.IntN(1_000_000_000)) + return "" + } + return "ci" + runID + "x" +} + +// newBundleNamePrefix builds the "cix" prefix that ciUniqueName +// stamps into every $UNIQUE_NAME so deployed bundles can be attributed and swept. +// It uses the GitHub run id (via ciRunPrefix), or a random numeric id when that is +// unavailable (e.g. a local `deco env run`) so the name is still uniquely shaped +// and reclaimed by the periodic sweeper. See bundleLegSuffixLen for the suffix. +func newBundleNamePrefix() string { + prefix := ciRunPrefix() + if prefix == "" { + prefix = "ci" + strconv.Itoa(rand.IntN(1_000_000_000)) + "x" } const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" suffix := make([]byte, bundleLegSuffixLen) for i := range suffix { suffix[i] = alphabet[rand.IntN(len(alphabet))] } - return "ci" + runID + "x" + string(suffix) + return prefix + string(suffix) } // ciUniqueName prepends prefix to the random unique name, preserving its length diff --git a/acceptance/bundle_clean_test.go b/acceptance/bundle_clean_test.go index bb1392173f1..e97b9238005 100644 --- a/acceptance/bundle_clean_test.go +++ b/acceptance/bundle_clean_test.go @@ -7,6 +7,7 @@ import ( "os/exec" "path" "path/filepath" + "runtime" "slices" "strings" "sync" @@ -17,25 +18,42 @@ import ( "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/workspace" + "github.com/stretchr/testify/require" ) -// setupBundleCleanup arranges for every bundle deployed by this run to be -// destroyed once the suite finishes. The caller invokes it only on cloud: all -// cloud tests share one real workspace, whereas local tests each get a -// throwaway in-memory fake workspace with nothing to clean up. +// TestCleanupLeakedBundles destroys every bundle a cloud run deployed. It is the +// run's cleanup step: unlike a t.Cleanup — which go test skips when the suite +// panics on -timeout or the CI job is killed at its time limit — this runs as a +// separate, always()-invoked job after every matrix leg finishes, so leaked +// deployments are reclaimed even when a test job times out. // -// prefix is the leg-specific "cix" that ciUniqueName stamps -// into every $UNIQUE_NAME, so the cleanup sweeps exactly the deployments this -// leg created and nothing else. That is what makes destroying against the shared -// workspace safe even while sibling matrix legs (which share the run id and may -// share the workspace) deploy concurrently. -func setupBundleCleanup(t *testing.T, execPath, prefix string) { - // t.Context() is canceled once the test finishes, before cleanups run, so - // derive a context that survives cancellation for the cleanup's API calls. - ctx := context.WithoutCancel(t.Context()) - t.Cleanup(func() { - cleanBundles(ctx, t, execPath, prefix) - }) +// It sweeps the run-wide "cix" prefix that ciUniqueName stamps into +// every $UNIQUE_NAME, so it matches all legs of the run. That is safe only because +// it runs once every leg has finished and no bundle is still live. It is gated +// behind CLEANUP_LEAKED_BUNDLES so it never runs inline in a normal suite, where a +// run-wide sweep would destroy sibling legs' live deployments. +func TestCleanupLeakedBundles(t *testing.T) { + if os.Getenv("CLEANUP_LEAKED_BUNDLES") == "" { + t.Skip("set CLEANUP_LEAKED_BUNDLES=1 to run the post-run bundle sweep") + } + if os.Getenv("CLOUD_ENV") == "" { + t.Skip("bundle cleanup only applies to cloud runs") + } + + prefix := ciRunPrefix() + require.NotEmpty(t, prefix, "GITHUB_RUN_ID must be a valid run id so the sweep knows which bundles to destroy") + + cwd, err := os.Getwd() + require.NoError(t, err) + + // Reuse a prebuilt CLI when given (-clipath), else build from the current source. + execPath := CLIPath + if execPath == "" { + buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH) + execPath = BuildCLI(t, buildDir, "", runtime.GOOS, runtime.GOARCH) + } + + cleanBundles(t.Context(), t, execPath, prefix) } // cleanBundles finds every bundle this run deployed under the current user's @@ -113,12 +131,10 @@ func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { slices.Sort(failed) t.Logf("%s bundle cleanup: destroyed %d/%d deployment(s) in %s", time.Now().Format(time.RFC3339), len(roots)-len(failed), len(roots), time.Since(start)) - // Do not fail the test on a cleanup failure: this runs in a t.Cleanup on the - // root TestAccept, so failing here marks the root test failed with no failed - // subtest, which makes gotestsum --rerun-fails (used by the integration task) - // rerun the entire cloud suite. Cleanup is best-effort housekeeping and the - // product tests already passed, so log loudly instead; leaked deployments are - // reclaimed by the periodic prefix sweep (sweep_test_resources.py). + // Do not fail on a cleanup failure: this is best-effort housekeeping that runs + // after the product tests, so a transient destroy failure should not turn the + // cleanup step red and mask the real test signal. Log loudly instead; leaked + // deployments are reclaimed by the periodic prefix sweep (sweep_test_resources.py). if len(failed) > 0 { t.Logf("WARNING: bundle cleanup failed to destroy %d deployment(s), leaked until swept: %s", len(failed), strings.Join(failed, ", ")) } From 9e6a9d1ffb584e26d952671ca13e2357734201af Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 8 Sep 2026 21:01:34 +0200 Subject: [PATCH 2/3] convert to cleanup binary --- .gitignore | 1 + acceptance/acceptance_test.go | 15 +-- .../{bundle_clean_test.go => cleanup/main.go} | 105 ++++++++---------- 3 files changed, 51 insertions(+), 70 deletions(-) rename acceptance/{bundle_clean_test.go => cleanup/main.go} (55%) diff --git a/.gitignore b/.gitignore index 4b82c6d1521..fc0194ad3a4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Root binary from a bare `go build`; anchored so it doesn't also ignore # nested paths like .nextchanges/cli/. /cli +/cleanup # Test binary, built with `go test -c` *.test diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 41399588958..8e813c2c94d 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -734,11 +734,10 @@ var bundleNamePrefix string // ciRunPrefix returns the run-wide "cix" prefix that attributes every // bundle a cloud run deploys to its GitHub run, so they can be swept by -// TestCleanupLeakedBundles and tools/sweep_test_resources.py. The run id (all -// digits) is delimited by "x" so the prefix stays collision-free between runs -// whose ids share a leading substring. Returns "" when GITHUB_RUN_ID is unset or -// not a valid numeric id (e.g. a local `deco env run`), where there is no shared -// run id to key attribution on. +// acceptance/cleanup and tools/sweep_test_resources.py. The run id (all digits) +// is delimited by "x" so the prefix stays collision-free between runs whose ids +// share a leading substring. Returns "" when GITHUB_RUN_ID is unset or not a +// valid numeric id (e.g. a local `deco env run`). func ciRunPrefix() string { runID := os.Getenv("GITHUB_RUN_ID") if !ciRunID.MatchString(runID) { @@ -749,13 +748,11 @@ func ciRunPrefix() string { // newBundleNamePrefix builds the "cix" prefix that ciUniqueName // stamps into every $UNIQUE_NAME so deployed bundles can be attributed and swept. -// It uses the GitHub run id (via ciRunPrefix), or a random numeric id when that is -// unavailable (e.g. a local `deco env run`) so the name is still uniquely shaped -// and reclaimed by the periodic sweeper. See bundleLegSuffixLen for the suffix. +// Returns "" on non-CI runs (no GITHUB_RUN_ID), where cleanup is not automatic. func newBundleNamePrefix() string { prefix := ciRunPrefix() if prefix == "" { - prefix = "ci" + strconv.Itoa(rand.IntN(1_000_000_000)) + "x" + return "" } const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" suffix := make([]byte, bundleLegSuffixLen) diff --git a/acceptance/bundle_clean_test.go b/acceptance/cleanup/main.go similarity index 55% rename from acceptance/bundle_clean_test.go rename to acceptance/cleanup/main.go index e97b9238005..c98c3743280 100644 --- a/acceptance/bundle_clean_test.go +++ b/acceptance/cleanup/main.go @@ -1,79 +1,67 @@ -package acceptance_test +// Package main implements a standalone bundle cleanup program. It is invoked as a +// separate always()-triggered workflow job (see cli-isolated-tests.yml in +// databricks-eng/eng-dev-ecosystem) so it runs even when a test job times out or +// is cancelled — unlike a t.Cleanup, which go test skips in those cases. +package main import ( "context" "errors" + "flag" + "fmt" + "log" "os" "os/exec" "path" "path/filepath" - "runtime" + "regexp" "slices" "strings" "sync" - "testing" "time" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/workspace" - "github.com/stretchr/testify/require" ) -// TestCleanupLeakedBundles destroys every bundle a cloud run deployed. It is the -// run's cleanup step: unlike a t.Cleanup — which go test skips when the suite -// panics on -timeout or the CI job is killed at its time limit — this runs as a -// separate, always()-invoked job after every matrix leg finishes, so leaked -// deployments are reclaimed even when a test job times out. -// -// It sweeps the run-wide "cix" prefix that ciUniqueName stamps into -// every $UNIQUE_NAME, so it matches all legs of the run. That is safe only because -// it runs once every leg has finished and no bundle is still live. It is gated -// behind CLEANUP_LEAKED_BUNDLES so it never runs inline in a normal suite, where a -// run-wide sweep would destroy sibling legs' live deployments. -func TestCleanupLeakedBundles(t *testing.T) { - if os.Getenv("CLEANUP_LEAKED_BUNDLES") == "" { - t.Skip("set CLEANUP_LEAKED_BUNDLES=1 to run the post-run bundle sweep") - } - if os.Getenv("CLOUD_ENV") == "" { - t.Skip("bundle cleanup only applies to cloud runs") - } +// ciRunID matches a valid GITHUB_RUN_ID (same cap as acceptance_test.go). +var ciRunID = regexp.MustCompile(`^[0-9]{1,11}$`) - prefix := ciRunPrefix() - require.NotEmpty(t, prefix, "GITHUB_RUN_ID must be a valid run id so the sweep knows which bundles to destroy") +func main() { + var cliPath string + flag.StringVar(&cliPath, "cli", "", "path to databricks CLI binary (required)") + flag.Parse() - cwd, err := os.Getwd() - require.NoError(t, err) + if cliPath == "" { + log.Fatal("-cli: path to databricks CLI binary is required") + } - // Reuse a prebuilt CLI when given (-clipath), else build from the current source. - execPath := CLIPath - if execPath == "" { - buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH) - execPath = BuildCLI(t, buildDir, "", runtime.GOOS, runtime.GOARCH) + runID := os.Getenv("GITHUB_RUN_ID") + if !ciRunID.MatchString(runID) { + log.Fatalf("GITHUB_RUN_ID %q is not a valid run id (must be 1-11 digits)", runID) } + prefix := "ci" + runID + "x" - cleanBundles(t.Context(), t, execPath, prefix) + if err := cleanBundles(context.Background(), cliPath, prefix); err != nil { + log.Fatal(err) + } } // cleanBundles finds every bundle this run deployed under the current user's -// ~/.bundle directory (identified by the run's prefix) and destroys each one, -// logging each deployment and the total time taken. -func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { +// ~/.bundle directory (identified by the run's prefix) and destroys each one. +func cleanBundles(ctx context.Context, execPath, prefix string) error { start := time.Now() - // Cleanup never fails the test (see the WARNING note below), so on any error - // that prevents sweeping, log loudly and return rather than require-failing. w, err := databricks.NewWorkspaceClient() if err != nil { - t.Logf("WARNING: bundle cleanup skipped, cannot create client: %s", err) - return + return fmt.Errorf("cannot create workspace client: %w", err) } me, err := w.CurrentUser.Me(ctx, iam.MeRequest{}) if err != nil { - t.Logf("WARNING: bundle cleanup skipped, cannot resolve current user: %s", err) - return + return fmt.Errorf("cannot resolve current user: %w", err) } // Tests deploy under the user's home .bundle by default, but some set @@ -91,15 +79,15 @@ func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { // thousands of directories other runs may have leaked under .bundle. var roots []string for _, bundleRoot := range bundleRoots { - for _, child := range listChildDirs(ctx, t, w, bundleRoot) { + for _, child := range listChildDirs(ctx, w, bundleRoot) { if strings.Contains(path.Base(child), prefix) { - roots = append(roots, findDeploymentRoots(ctx, t, w, child)...) + roots = append(roots, findDeploymentRoots(ctx, w, child)...) } } } slices.Sort(roots) - t.Logf("%s bundle cleanup: found %d deployment(s) with prefix %q", time.Now().Format(time.RFC3339), len(roots), prefix) + log.Printf("bundle cleanup: found %d deployment(s) with prefix %q", len(roots), prefix) // Each destroy shells out to a separate `bundle destroy` (auth + state pull + // deletes), so run them concurrently. Each is network-bound (not CPU-bound), @@ -117,9 +105,9 @@ func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { sem <- struct{}{} wg.Go(func() { defer func() { <-sem }() - t.Logf("%s destroying %s", time.Now().Format(time.RFC3339), root) + log.Printf("destroying %s", root) if out, err := destroyBundle(execPath, root); err != nil { - t.Logf("%s destroy failed: %s\n%s", time.Now().Format(time.RFC3339), root, out) + log.Printf("destroy failed: %s\n%s", root, out) mu.Lock() failed = append(failed, root) mu.Unlock() @@ -129,15 +117,11 @@ func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { wg.Wait() slices.Sort(failed) - t.Logf("%s bundle cleanup: destroyed %d/%d deployment(s) in %s", time.Now().Format(time.RFC3339), len(roots)-len(failed), len(roots), time.Since(start)) - - // Do not fail on a cleanup failure: this is best-effort housekeeping that runs - // after the product tests, so a transient destroy failure should not turn the - // cleanup step red and mask the real test signal. Log loudly instead; leaked - // deployments are reclaimed by the periodic prefix sweep (sweep_test_resources.py). + log.Printf("bundle cleanup: destroyed %d/%d deployment(s) in %s", len(roots)-len(failed), len(roots), time.Since(start)) if len(failed) > 0 { - t.Logf("WARNING: bundle cleanup failed to destroy %d deployment(s), leaked until swept: %s", len(failed), strings.Join(failed, ", ")) + return fmt.Errorf("failed to destroy %d deployment(s): %s", len(failed), strings.Join(failed, ", ")) } + return nil } // findDeploymentRoots walks the workspace tree under dir and returns the paths @@ -145,9 +129,9 @@ func cleanBundles(ctx context.Context, t *testing.T, execPath, prefix string) { // a "state" or "files" child, which the bundle deploy writes beneath the // resolved workspace.root_path. This works regardless of whether the root is // the default ~/.bundle// or a custom ~/.bundle/<...> override. -func findDeploymentRoots(ctx context.Context, t *testing.T, w *databricks.WorkspaceClient, dir string) []string { +func findDeploymentRoots(ctx context.Context, w *databricks.WorkspaceClient, dir string) []string { var childDirs []string - for _, child := range listChildDirs(ctx, t, w, dir) { + for _, child := range listChildDirs(ctx, w, dir) { if base := path.Base(child); base == "state" || base == "files" { // dir is a deployment root; don't descend into its internals. return []string{dir} @@ -157,20 +141,19 @@ func findDeploymentRoots(ctx context.Context, t *testing.T, w *databricks.Worksp var roots []string for _, child := range childDirs { - roots = append(roots, findDeploymentRoots(ctx, t, w, child)...) + roots = append(roots, findDeploymentRoots(ctx, w, child)...) } return roots } // listChildDirs returns the immediate subdirectory paths of dir. A missing dir // (nothing was deployed under it) yields nil silently; any other listing error -// is logged loudly (it means the sweep under dir is incomplete) but does not -// fail the test, since cleanup runs in the root t.Cleanup. -func listChildDirs(ctx context.Context, t *testing.T, w *databricks.WorkspaceClient, dir string) []string { +// is logged loudly but does not stop the overall sweep. +func listChildDirs(ctx context.Context, w *databricks.WorkspaceClient, dir string) []string { objects, err := w.Workspace.ListAll(ctx, workspace.ListWorkspaceRequest{Path: dir}) if err != nil { if !errors.Is(err, apierr.ErrNotFound) { - t.Logf("WARNING: bundle cleanup incomplete, cannot list %s: %s", dir, err) + log.Printf("WARNING: bundle cleanup incomplete, cannot list %s: %s", dir, err) } return nil } @@ -192,7 +175,7 @@ func listChildDirs(ctx context.Context, t *testing.T, w *databricks.WorkspaceCli // stale deployment lock left by a test that was killed mid-deploy; these are // known-leaked bundles, so there is no concurrent deployment to conflict with. func destroyBundle(execPath, rootPath string) ([]byte, error) { - dir, err := os.MkdirTemp("", "bundle-clean") //nolint:usetesting // runs in a cleanup, where t.TempDir is already removed + dir, err := os.MkdirTemp("", "bundle-clean") if err != nil { return nil, err } From 90f76cd46307b2ad1030a58afa662d455735e6d1 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 9 Sep 2026 10:40:28 +0200 Subject: [PATCH 3/3] fix lint --- acceptance/cleanup/main.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/acceptance/cleanup/main.go b/acceptance/cleanup/main.go index c98c3743280..d7e914a5f98 100644 --- a/acceptance/cleanup/main.go +++ b/acceptance/cleanup/main.go @@ -9,7 +9,6 @@ import ( "errors" "flag" "fmt" - "log" "os" "os/exec" "path" @@ -20,6 +19,8 @@ import ( "sync" "time" + "github.com/databricks/cli/libs/env" + "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/iam" @@ -30,22 +31,23 @@ import ( var ciRunID = regexp.MustCompile(`^[0-9]{1,11}$`) func main() { + ctx := context.Background() var cliPath string flag.StringVar(&cliPath, "cli", "", "path to databricks CLI binary (required)") flag.Parse() if cliPath == "" { - log.Fatal("-cli: path to databricks CLI binary is required") + log.Errorf(ctx, "-cli: path to databricks CLI binary is required") } - runID := os.Getenv("GITHUB_RUN_ID") + runID := env.Get(ctx, "GITHUB_RUN_ID") if !ciRunID.MatchString(runID) { - log.Fatalf("GITHUB_RUN_ID %q is not a valid run id (must be 1-11 digits)", runID) + log.Errorf(ctx, "GITHUB_RUN_ID %q is not a valid run id (must be 1-11 digits)", runID) } prefix := "ci" + runID + "x" - if err := cleanBundles(context.Background(), cliPath, prefix); err != nil { - log.Fatal(err) + if err := cleanBundles(ctx, cliPath, prefix); err != nil { + log.Errorf(ctx, "failed to clean bundles: %s", err) } } @@ -87,7 +89,7 @@ func cleanBundles(ctx context.Context, execPath, prefix string) error { } slices.Sort(roots) - log.Printf("bundle cleanup: found %d deployment(s) with prefix %q", len(roots), prefix) + log.Infof(ctx, "bundle cleanup: found %d deployment(s) with prefix %q", len(roots), prefix) // Each destroy shells out to a separate `bundle destroy` (auth + state pull + // deletes), so run them concurrently. Each is network-bound (not CPU-bound), @@ -105,9 +107,9 @@ func cleanBundles(ctx context.Context, execPath, prefix string) error { sem <- struct{}{} wg.Go(func() { defer func() { <-sem }() - log.Printf("destroying %s", root) + log.Infof(ctx, "destroying %s", root) if out, err := destroyBundle(execPath, root); err != nil { - log.Printf("destroy failed: %s\n%s", root, out) + log.Infof(ctx, "destroy failed: %s\n%s", root, out) mu.Lock() failed = append(failed, root) mu.Unlock() @@ -117,7 +119,7 @@ func cleanBundles(ctx context.Context, execPath, prefix string) error { wg.Wait() slices.Sort(failed) - log.Printf("bundle cleanup: destroyed %d/%d deployment(s) in %s", len(roots)-len(failed), len(roots), time.Since(start)) + log.Infof(ctx, "bundle cleanup: destroyed %d/%d deployment(s) in %s", len(roots)-len(failed), len(roots), time.Since(start)) if len(failed) > 0 { return fmt.Errorf("failed to destroy %d deployment(s): %s", len(failed), strings.Join(failed, ", ")) } @@ -153,7 +155,7 @@ func listChildDirs(ctx context.Context, w *databricks.WorkspaceClient, dir strin objects, err := w.Workspace.ListAll(ctx, workspace.ListWorkspaceRequest{Path: dir}) if err != nil { if !errors.Is(err, apierr.ErrNotFound) { - log.Printf("WARNING: bundle cleanup incomplete, cannot list %s: %s", dir, err) + log.Infof(ctx, "WARNING: bundle cleanup incomplete, cannot list %s: %s", dir, err) } return nil }