Skip to content

fix(sandbox): autostash rebase to survive concurrent generated-file writes#4261

Merged
guitavano merged 1 commit into
mainfrom
guitavano/fix-rebase-force-push-dataloss
Jul 2, 2026
Merged

fix(sandbox): autostash rebase to survive concurrent generated-file writes#4261
guitavano merged 1 commit into
mainfrom
guitavano/fix-rebase-force-push-dataloss

Conversation

@guitavano

@guitavano guitavano commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the could not detach HEAD failure hit during the sandbox one-click Publish flow (push → rebase-onto-base).

The running dev server keeps regenerating git-tracked files (src/server/cms/blocks.gen.json, .deco/blocks/*.json). It can dirty the working tree in the window between commitBeforeRebase and the rebase's internal checkout, so git aborts with:

error: Your local changes to the following files would be overwritten by checkout:
  src/server/cms/blocks.gen.json
... could not detach HEAD

(Retrying often works purely by timing.) Adding --autostash stashes that churn across the rebase and restores it afterward.

Change

One line in rebaseOntoBaseInner:

-    runGit(repoDir, ["rebase", "-X", "theirs", upstream]);
+    runGit(repoDir, ["rebase", "--autostash", "-X", "theirs", upstream]);

Test

  • bun test packages/sandbox/daemon/git/rebase-onto-base.test.ts — passes
  • bun run --cwd=apps/mesh check (tsc) — passes
  • bun run fmt — clean

Scope / follow-up (intentionally not here)

Diagnosis of the same publish flow surfaced a separate, more serious data-loss mode: under dev-server instability the rebase could end with a HEAD that dropped the branch's real changes, and the unconditional forcePushRebasedBranch force-pushed it — moving origin/<branch> backwards onto an ancestor of the pushed commit (confirmed on GitHub: branches rho-hydrae/kappa-lyrae ended at ancestors of commits eb042f6/c3f5ac49c that never reached main). A guard for that is being deferred to a follow-up because it needs care to avoid false positives in the legitimate "already published, nothing new" case. The deeper trigger (dev server writing tracked files during git ops) also warrants a durable fix (quiesce generation / separate worktree during publish/rebase).

🤖 Generated with Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sandbox/daemon/git/rebase-onto-base.ts">

<violation number="1" location="packages/sandbox/daemon/git/rebase-onto-base.ts:286">
P1: This guard still allows branch data loss when rebased HEAD differs from base for unrelated reasons but no longer contains the remote branch’s actual changes. Comparing patch equivalence of remote-vs-base and HEAD-vs-base (not just whether each is non-empty) would make the force-push refusal reliable.</violation>

<violation number="2" location="packages/sandbox/daemon/git/rebase-onto-base.ts:404">
P2: If the hard reset fails, local HEAD can remain in the collapsed state even though the thrown message says it was restored. Using `runGit` here (or explicitly checking the `tryGit` result) keeps recovery behavior and messaging accurate.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

tryGit(repoDir, ["diff", "--quiet", upstream, remoteBranchHead]) === null;
const headDiffersFromBase =
tryGit(repoDir, ["diff", "--quiet", upstream, "HEAD"]) === null;
return remoteDiffersFromBase && !headDiffersFromBase;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This guard still allows branch data loss when rebased HEAD differs from base for unrelated reasons but no longer contains the remote branch’s actual changes. Comparing patch equivalence of remote-vs-base and HEAD-vs-base (not just whether each is non-empty) would make the force-push refusal reliable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sandbox/daemon/git/rebase-onto-base.ts, line 286:

<comment>This guard still allows branch data loss when rebased HEAD differs from base for unrelated reasons but no longer contains the remote branch’s actual changes. Comparing patch equivalence of remote-vs-base and HEAD-vs-base (not just whether each is non-empty) would make the force-push refusal reliable.</comment>

<file context>
@@ -256,6 +256,36 @@ function remoteBranchSha(repoDir: string, branch: string): string | null {
+    tryGit(repoDir, ["diff", "--quiet", upstream, remoteBranchHead]) === null;
+  const headDiffersFromBase =
+    tryGit(repoDir, ["diff", "--quiet", upstream, "HEAD"]) === null;
+  return remoteDiffersFromBase && !headDiffersFromBase;
+}
+
</file context>

// Restore the local checkout to the intact remote branch so a follow-up
// publish can't re-push the collapsed state, and refuse to force-push.
// The remote branch (with the user's commits) is left untouched.
tryGit(repoDir, ["reset", "--hard", leaseSha]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: If the hard reset fails, local HEAD can remain in the collapsed state even though the thrown message says it was restored. Using runGit here (or explicitly checking the tryGit result) keeps recovery behavior and messaging accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sandbox/daemon/git/rebase-onto-base.ts, line 404:

<comment>If the hard reset fails, local HEAD can remain in the collapsed state even though the thrown message says it was restored. Using `runGit` here (or explicitly checking the `tryGit` result) keeps recovery behavior and messaging accurate.</comment>

<file context>
@@ -361,6 +397,18 @@ function rebaseOntoBaseInner(
+    // Restore the local checkout to the intact remote branch so a follow-up
+    // publish can't re-push the collapsed state, and refuse to force-push.
+    // The remote branch (with the user's commits) is left untouched.
+    tryGit(repoDir, ["reset", "--hard", leaseSha]);
+    throw new Error(
+      "Refusing to force-push: rebase would discard the branch's committed " +
</file context>
Suggested change
tryGit(repoDir, ["reset", "--hard", leaseSha]);
runGit(repoDir, ["reset", "--hard", leaseSha]);

…rites

The sandbox one-click publish runs push → rebase-onto-base. The running dev
server keeps regenerating git-tracked files (src/server/cms/blocks.gen.json,
.deco/blocks/*.json) and can dirty the working tree in the window between
commitBeforeRebase and the rebase's internal checkout. Git then aborts with
"Your local changes to the following files would be overwritten by checkout …
could not detach HEAD" (retrying often works by luck of timing).

Add --autostash so that churn is stashed across the rebase and restored after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@guitavano guitavano force-pushed the guitavano/fix-rebase-force-push-dataloss branch from 3ba181a to d157a90 Compare July 2, 2026 16:14
@guitavano guitavano changed the title fix(sandbox): prevent rebase from wiping pushed commits & racing generated files fix(sandbox): autostash rebase to survive concurrent generated-file writes Jul 2, 2026
@guitavano guitavano merged commit 3009362 into main Jul 2, 2026
13 checks passed
@guitavano guitavano deleted the guitavano/fix-rebase-force-push-dataloss branch July 2, 2026 16:31
guitavano added a commit that referenced this pull request Jul 2, 2026
Recovers the auto-bump missed for #4261 (autostash rebase fix): the release
bot ran on the merge commit before the GitHub API associated the PR, logged
"No merged PR associated … skipping release bump", and left the sandbox at
1.8.2 — so no 1.8.3 image was published.

Bump packages/sandbox/package.json to 1.8.3 so release-studio-sandbox builds
and pushes studio-sandbox:1.8.3. The [release]: prefix makes release-tagging
skip re-bumping this commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant