Skip to content

[bug] Deleting a workspace leaves its git branch behind, so reusing the workspace name creates a mismatched branch #6380

Open

Description

@cry0genic

What happened

superset ws delete removes the workspace record and the worktree directory, but it leaves the local git branch behind in the parent repository.

Because the branch still exists, creating a new workspace with the same name does not reuse the name. Superset silently picks a suffixed branch instead: <name>-2, then <name>-3, and so on. The workspace name stays the same, so the workspace name and its branch no longer agree.

In the desktop app the same flow is worse: after I delete a workspace and create a new one with the same name, the app lands on a "Workspace not found" screen showing the old workspace ID (aa511abc-07ec-41e6-9976-45e603fb170b), and I cannot get into the new workspace.

I expect a deleted workspace to release its branch, so the same name is genuinely free to use again.

Steps to reproduce

  1. Create a workspace:
    superset ws create --local --project <id> --name dupe-repro --branch dupe-repro
    Result: "branch": "cry0genic/dupe-repro"
  2. Delete it: superset ws delete <workspace-id> --local
    Result: {"deleted": ["..."], "warnings": []} — no warning about the branch.
  3. Run git branch --list 'cry0genic/dupe*' in the parent repository.
    Result: cry0genic/dupe-repro is still there.
  4. Create the workspace again with the identical name and branch.
    Result: "name": "dupe-repro" but "branch": "cry0genic/dupe-repro-2".
  5. Repeat steps 2-4. The suffix increments to -3, and every deleted branch stays.

Observed branch list after three cycles:

cry0genic/dupe-repro
cry0genic/dupe-repro-2
cry0genic/dupe-repro-3

The worktree directories under ~/.superset/worktrees/<project>/ are removed correctly, and the project teardown script runs correctly. Only the branch survives.

Expected

One of:

  • Delete the branch with the workspace when the branch holds no unmerged work, or
  • Ask the user whether to keep or delete the branch, or
  • At minimum, report a warning that names the leftover branch, instead of silently renaming the new workspace's branch.

Environment

  • Superset CLI: 1.20.2
  • Superset desktop app: 1.20.2
  • OS: macOS (Darwin arm64)
  • Workspace type: worktree

Related

Activity

  1. github-actions commented on Aug 12, 2026

    @github-actions
    Contributor

    Summary

    The report is consistent with the code. Local branch deletion on workspace destroy is strictly opt-in (deleteBranch), and the non-interactive surface used by superset ws delete hardcodes it to false with no CLI flag and no way to observe the leftover ref — so the branch always survives, and the next create with the same name collides with it and gets deduplicated to -2/-3. The desktop "Workspace not found showing the old workspace ID" looks like a second, separate bug rather than the same symptom: deleted workspaces are tombstoned (not deleted) rows, and the create-time branch idempotency lookup does not exclude tombstones, so a create that resolves to a tombstoned row's branch returns the old, archived workspace id, which the renderer then navigates to and the workspace list can't resolve.

    Affected code

    Branch survives the delete

    • packages/cli/src/commands/workspaces/delete/command.ts:9-12 — options are only --host / --local; there is no branch flag, and command.ts:35 calls workspace.delete.mutate({ id }).
    • packages/host-service/src/trpc/router/workspace/workspace.ts:178-192 — the legacy external surface (CLI/SDK/MCP) hardcodes deleteBranch: false, force: true, teardownMode: "best-effort".
    • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:514-532 — step 5 runs only when input.deleteBranch is set, and pushes a warning only when the delete throws. With deleteBranch: false there is no code path that can produce a warning about the surviving ref, which matches the observed "warnings": [].
    • packages/host-service/src/runtime/archived-workspace-reconcile.ts:52-57 — the crash reconciler also passes deleteBranch: false, so nothing else reclaims it later.
    • Desktop's checkbox is backed by a preference that defaults to false (apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts:360), so the same leftover happens in the app unless the user opts in.

    The -2 suffix

    • packages/host-service/src/trpc/router/workspaces/workspaces.ts:840-874 — for a typed --branch dupe-repro, planBranchSource looks for a ref literally named dupe-repro. The leftover ref is the prefixed cry0genic/dupe-repro, so no existing branch is found → usedExistingBranch: false → the project prefix is applied → deduplicateBranchName("cry0genic/dupe-repro", existing).
    • packages/host-service/src/trpc/router/workspace-creation/utils/sanitize-branch.ts:35-38 — appends -2, -3, … against the live branch list. input.name is never deduplicated, which is exactly the reported name/branch divergence (and the worktree dir becomes .../cry0genic/dupe-repro-2, since the path is derived from the resolved branch at workspaces.ts:943-947).

    Desktop "Workspace not found" with the old ID

    • packages/host-service/src/trpc/router/workspaces/workspaces.ts:156-170 — findExistingWorkspaceByBranch matches on (projectId, branch) with no isNull(workspaces.archivedAt) filter, so it can return a tombstoned row and workspaces.ts:907-914 sets alreadyExists = true on it.
    • Compare packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts:123, :142, :223 — the adopt path does exclude tombstones ("Tombstones aren't phantoms — same-branch history must survive re-creating a workspace on that branch"). The two lookups disagree.
    • packages/host-service/src/workspaces/local-workspace-store.ts:227-254 — delete sets archivedAt; the row is never purged.
    • apps/desktop/.../useSubmitWorkspace/useSubmitWorkspace.ts:166-183 — "The server can resolve the optimistic workspace to a different canonical id; follow it only if we're still on the optimistic route" → the renderer navigates to whatever id create returned, including a tombstone's.
    • packages/host-service/src/trpc/router/workspace/workspace.ts:43-52 — list filters archivedAt IS NULL by default, so the layout at apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx:83-96 finds nothing and renders WorkspaceNotFoundState with that stale id.

    This second path triggers whenever the create resolves to the exact branch string of a tombstoned row — e.g. typing or picking the full prefixed cry0genic/dupe-repro (which the branch picker still offers, precisely because step 5 left the ref behind). If in your desktop repro you left the branch auto-derived from the name, dedupe would have produced -2 and a fresh row instead, and the not-found screen would need a different explanation — the create response (workspace.id, alreadyExists) and the branch shown in the modal at submit time would settle it.

    Ruled out

    • Worktree removal and teardown: step 3b removes the worktree and verifies deregistration (workspace-cleanup.ts:465-493), consistent with "worktree directories are removed correctly".
    • gitDeleteBranchTask itself (packages/host-service/src/workers/tasks/git.ts:158-174) is fine — it probes branch --list and then branch -D. It simply is never invoked on this path.

    Reproduction

    CLI/host level, in the style of packages/host-service/test/integration/workspace-create-delete.integration.test.ts:

    // 1. leftover branch
    const first = await scenario.host.trpc.workspaces.create.mutate({
      projectId: scenario.projectId, name: "dupe-repro", branch: "dupe-repro",
    });
    await scenario.host.trpc.workspace.delete.mutate({ id: first.workspace.id });
    expect((await scenario.repo.git.branchLocal()).all)
      .not.toContain(first.workspace.branch);            // currently FAILS
    
    // 2. suffix drift
    const second = await scenario.host.trpc.workspaces.create.mutate({
      projectId: scenario.projectId, name: "dupe-repro", branch: "dupe-repro",
    });
    expect(second.workspace.branch).toBe(first.workspace.branch); // currently -2
    
    // 3. tombstone idempotency (independent of 1 & 2)
    const third = await scenario.host.trpc.workspaces.create.mutate({
      projectId: scenario.projectId, name: "dupe-repro",
      branch: first.workspace.branch,                    // exact archived branch
    });
    expect(third.workspace.id).not.toBe(first.workspace.id);      // currently FAILS

    Test 3 is the one that reproduces the desktop symptom at the host level, and it fails independently of whether the branch is cleaned up.

    Suggested fix direction

    1. Never return a tombstone from create — add isNull(workspaces.archivedAt) to findExistingWorkspaceByBranch (workspaces.ts:161-168) so it matches adoptExistingWorktree's existing convention. This is the smallest, highest-value change: it stops the app from routing users to a deleted workspace id.
    2. Always report the surviving branch. In workspace-cleanup.ts:514-532, when deleteBranch is false (or the delete is skipped), probe the ref and push a warning naming it — the branch --list check inside gitDeleteBranchTask is already the right primitive to factor out. That alone satisfies the "at minimum" ask and makes the CLI's empty warnings array honest.
    3. Give the CLI the opt-in. Add --delete-branch to packages/cli/src/commands/workspaces/delete/command.ts and thread it through workspace.delete's input (defaulting to false to preserve the current contract) into destroyWorkspace.
    4. Optional, for the "delete when no unmerged work" variant: workspaceCleanup.inspect already computes hasUnpushedCommits (workspace-cleanup.ts:123-136), so an auto-delete could be gated on it — though note step 5 deliberately uses -D today because the checkbox is the consent, so any auto-delete should be a distinct policy rather than a change to that flag's semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions