From 1a4433639d5c02090e024f2e2a039c383d0b92f3 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 30 Jul 2026 21:25:22 -0500 Subject: [PATCH] fix(workspace): delete cached content folder on workspace delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit devsy workspace delete removed the container and the workspace's config/metadata folder, but left the cloned repo content in contexts//contents/ untouched — kept there intentionally so a later `up` on the same workspace could skip re-cloning. That's a reasonable optimization for `up --reset`, but "delete" silently leaving a full repo checkout (with potential embedded git credentials) on disk forever, with no cleanup path, is not. Have delete also remove ContentFolder, unless it's the user's own local folder mounted directly rather than a devsy-managed copy. --- cmd/internal/agentworkspace/delete.go | 24 ++++++++++- cmd/internal/agentworkspace/delete_test.go | 47 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/cmd/internal/agentworkspace/delete.go b/cmd/internal/agentworkspace/delete.go index 328c22571..bd35e68ec 100644 --- a/cmd/internal/agentworkspace/delete.go +++ b/cmd/internal/agentworkspace/delete.go @@ -91,11 +91,31 @@ func (cmd *DeleteCmd) Run(ctx context.Context) error { } } - // delete workspace folder + removeWorkspaceFolders(workspaceInfo) + + return nil +} + +// removeWorkspaceFolders deletes the workspace's config folder and cached +// content folder, logging rather than failing on either error since deletion +// should proceed best-effort once the container/daemon are already gone. +func removeWorkspaceFolders(workspaceInfo *provider2.AgentWorkspaceInfo) { if err := forceRemoveAll(workspaceInfo.Origin); err != nil { log.Errorf("remove workspace folder: %v", err) } - return nil + if err := removeContentFolder(workspaceInfo); err != nil { + log.Errorf("remove workspace content folder: %v", err) + } +} + +// removeContentFolder deletes the workspace's cached content folder, unless +// it's the user's own local folder mounted directly rather than a devsy copy. +func removeContentFolder(workspaceInfo *provider2.AgentWorkspaceInfo) error { + if workspaceInfo.ContentFolder == "" || + workspaceInfo.ContentFolder == workspaceInfo.Workspace.Source.LocalFolder { + return nil + } + return forceRemoveAll(workspaceInfo.ContentFolder) } func removeContainer( diff --git a/cmd/internal/agentworkspace/delete_test.go b/cmd/internal/agentworkspace/delete_test.go index 6a7d63a57..2adf84563 100644 --- a/cmd/internal/agentworkspace/delete_test.go +++ b/cmd/internal/agentworkspace/delete_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "runtime" "testing" + + provider2 "github.com/devsy-org/devsy/pkg/provider" ) func skipIfPermissionsNotEnforced(t *testing.T) { @@ -120,3 +122,48 @@ func TestForceRemoveAll_EmptyString(t *testing.T) { t.Fatalf("forceRemoveAll with empty string should not error: %v", err) } } + +func TestRemoveContentFolder_RemovesDevsyManagedContent(t *testing.T) { + dir := t.TempDir() + content := filepath.Join(dir, "contents", "my-workspace") + if err := os.MkdirAll(content, 0o750); err != nil { + t.Fatal(err) + } + + workspaceInfo := &provider2.AgentWorkspaceInfo{ + ContentFolder: content, + Workspace: &provider2.Workspace{}, + } + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder failed: %v", err) + } + assertRemoved(t, content) +} + +func TestRemoveContentFolder_SkipsUsersOwnLocalFolder(t *testing.T) { + dir := t.TempDir() + local := filepath.Join(dir, "my-project") + if err := os.MkdirAll(local, 0o750); err != nil { + t.Fatal(err) + } + + workspaceInfo := &provider2.AgentWorkspaceInfo{ + ContentFolder: local, + Workspace: &provider2.Workspace{ + Source: provider2.WorkspaceSource{LocalFolder: local}, + }, + } + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder failed: %v", err) + } + if _, err := os.Stat(local); err != nil { + t.Fatalf("expected local folder to survive, got stat err: %v", err) + } +} + +func TestRemoveContentFolder_EmptyContentFolder(t *testing.T) { + workspaceInfo := &provider2.AgentWorkspaceInfo{Workspace: &provider2.Workspace{}} + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder with empty ContentFolder should not error: %v", err) + } +}