Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

fix(code): chmod read-only subtrees before deleting worktrees - #1993

Merged
richardsolomou merged 3 commits into
mainfrom
posthog-code/fix-archive-readonly-worktrees
May 4, 2026
Merged

richardsolomou merged 3 commits into
mainfrom
posthog-code/fix-archive-readonly-worktrees

Conversation

@richardsolomou

Copy link
Copy Markdown
Member

Problem

Archiving (or suspending) a worktree that contained a Go module cache failed with EACCES: permission denied, rmdir '.../posthog/.flox/cache/go/pkg/mod/<pkg>/...'. Go's modcache marks every cached directory 0555, so fs.rm cannot unlink entries from those parents. The error propagated up archiveTask, the rollback chain undid the archive row, and the task reappeared in the sidebar — so to the user the archive button just... did nothing.

.flox/ exists in every flox-using worktree (even fresh ones, once anything is run), so this hit any task created in a flox-managed repo. Reported in Slack with multiple users blocked — "honestly kinda unusable".

Changes

  • Added forceRemove(path) in packages/git/src/utils.ts. Tries fs.rm once with retries; on EACCES/EPERM, walks the tree restoring write bits (dirs 0o700, files 0o600) and retries. Symlinks are skipped via lstat so we don't chmod through them.
  • Routed every worktree-deletion call site through forceRemove:
    • packages/git/src/worktree.ts — fallback when git worktree remove --force itself fails (it hits the same EACCES on the modcache and triggers the fallback path).
    • packages/git/src/sagas/worktree.ts — three rollback / execute spots in CreateWorktreeSaga, CreateWorktreeForBranchSaga, DeleteWorktreeSaga.
    • apps/code/src/main/services/archive/service.ts — parent-dir cleanup after deleteWorktree (both archive and rollback paths).
    • apps/code/src/main/services/suspension/service.ts — same pattern in deleteWorktreeOnDisk.

How did you test this?

  • New unit test packages/git/src/utils.test.ts reproduces the bug: builds a directory tree shaped like a Go modcache (yaml.v3@v3.0.1/.github/...) with parent dirs at 0555, asserts plain fs.rm rejects with EACCES, then asserts forceRemove succeeds. Also covers the empty-tree and missing-path cases.
  • Updated the suspension service test mock to expose the named exports of node:fs/promises that forceRemove needs (lstat, chmod, readdir).
  • pnpm --filter code test — 1007/1007 pass.
  • pnpm --filter code typecheck — clean.
  • pnpm --filter @posthog/git typecheck — clean.
  • pnpm format / pnpm lint — clean.

Publish to changelog?

no


Created with PostHog Code

Worktrees that ran any flox/Go workflow accumulate a module cache
under .flox/cache/go/pkg/mod with every directory marked 0555. Plain
fs.rm cannot unlink entries from those parents, so archive and
suspend would fail with EACCES, the rollback would undo the archive
row, and the task would reappear in the sidebar. Add forceRemove() to
walk the tree restoring write bits on EACCES/EPERM and retry, then
route every worktree-deletion call site through it.

Generated-By: PostHog Code
Task-Id: aa517cb8-e2ed-4a1c-bf8e-e592c652d690
@richardsolomou
richardsolomou requested a review from a team May 4, 2026 11:35
@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/git/src/utils.test.ts:28-67
**Prefer parameterised tests**

The project style guide asks for parameterised tests. The three `it` blocks ("removes a writable tree", "is a no-op for a missing path", and "removes a tree with read-only directories") share the same arrange-act-assert shape and could be expressed as a single `it.each` table, eliminating the repeated `workDir` / `forceRemove` / `fileExists` boilerplate.

### Issue 2 of 2
packages/git/src/utils.test.ts:19-26
**`afterEach` cleanup doesn't handle read-only subdirectories**

If `forceRemove` throws mid-way through the read-only test (leaving `yaml.v3@v3.0.1` still present with mode `0555`), `afterEach` does `chmod(workDir, 0o700)` — but only on `workDir` itself, not on its nested read-only children. The subsequent `rm(workDir, { recursive: true, force: true })` would then fail with `EACCES`, and the error is silently swallowed by `.catch(() => {})`, leaving a temp directory behind. Using `forceRemove` here instead closes the gap:

```suggestion
  afterEach(async () => {
    if (workDir) {
      await forceRemove(workDir).catch(() => {});
      workDir = undefined;
    }
  });
```

Reviews (1): Last reviewed commit: "fix(code): chmod read-only subtrees befo..." | Re-trigger Greptile

Comment thread packages/git/src/utils.test.ts Outdated
Comment thread packages/git/src/utils.test.ts
Comment thread packages/git/src/utils.ts
Use forceRemove in afterEach so cleanup handles read-only subdirs the
same way the code under test does. Collapse the two tree-removal cases
into a single it.each table to reduce boilerplate.

Generated-By: PostHog Code
Task-Id: aa517cb8-e2ed-4a1c-bf8e-e592c652d690
Silently swallowing these errors made it impossible to diagnose cases
where forceRemove's chmod pass itself failed (e.g. immutable flags or
ownership mismatches). Warn to console so the failure shows up in app
logs without breaking the flow.

Generated-By: PostHog Code
Task-Id: aa517cb8-e2ed-4a1c-bf8e-e592c652d690
@richardsolomou
richardsolomou enabled auto-merge (squash) May 4, 2026 11:48
@richardsolomou
richardsolomou disabled auto-merge May 4, 2026 11:48
@richardsolomou
richardsolomou enabled auto-merge (squash) May 4, 2026 11:49
@richardsolomou
richardsolomou merged commit fdaaf43 into main May 4, 2026
15 checks passed
@richardsolomou
richardsolomou deleted the posthog-code/fix-archive-readonly-worktrees branch May 4, 2026 11:52
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants