feat(workspaces): fork + push/pull#5210
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview New API surface under Sidebar/workspace header gains Manage Forks / Sync workspace (context menu + modals with mapping wizard and Activity tab). Deploy now rejects archived workflows under row lock. Subblock remap logic is extracted/shared and extended so cross-workspace copies clear unmapped workflow references; mapping API excludes user-editable Reviewed by Cursor Bugbot for commit 7635ad9. Configure here. |
Greptile SummaryThis PR introduces workspace forking: a new enterprise feature that lets users fork a workspace (copying its deployed workflows and selected resources into a child workspace) and then push or pull workflow state between the parent and child. It also adds rollback for the most recent promote and a mapping editor for configuring how credentials, secrets, and resources resolve across workspace boundaries.
Confidence Score: 4/5The fork and promote machinery is well-structured and the advisory-lock ordering is consistent across promote and rollback. One logic issue in drift detection could cause spurious blocked promotes in workspaces with native (non-forked) workflows, but the force:true path works around it so no data is at risk. Drift detection in computeForkPromotePlan compares updatedAt against the last promote timestamp for every non-archived workflow in the target, including ones never part of the fork mapping. In a shared production workspace where native workflows are actively developed, this triggers on every promote attempt, forcing users to always use force sync and defeating the purpose of the drift guard. apps/sim/lib/workspaces/fork/promote/promote-plan.ts — the drift detection query and comparison logic should be narrowed to only workflows that will actually be overwritten by the promote (replace-mode items). Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant ForkRoute as POST /fork
participant PromoteRoute as POST /fork/promote
participant RollbackRoute as POST /fork/rollback
participant DB as Database (tx)
participant BG as Background
Note over Client, BG: Fork flow
Client->>ForkRoute: "POST /workspaces/{id}/fork"
ForkRoute->>DB: insert workspace + permissions + workflows + resources + mappings
DB-->>ForkRoute: childWorkspaceId
ForkRoute->>BG: schedule forkContentCopy
ForkRoute-->>Client: 201 workspace
Note over Client, DB: Promote flow
Client->>PromoteRoute: "POST /fork/promote {direction, force}"
PromoteRoute->>DB: loadSourceDeployedStates (pre-tx)
PromoteRoute->>DB: tx: acquireTargetLock + edgeLock
PromoteRoute->>DB: computePromotePlan
PromoteRoute->>DB: copyWorkflowStateIntoTarget x N
PromoteRoute->>DB: upsertPromoteRun
DB-->>PromoteRoute: deployTargetIds
loop post-tx deploys
PromoteRoute->>DB: performFullDeploy
end
PromoteRoute-->>Client: 200 report
Note over Client, DB: Rollback flow
Client->>RollbackRoute: POST /fork/rollback
RollbackRoute->>DB: tx: acquireTargetLock + edgeLock
RollbackRoute->>DB: reactivateDeployedVersion / undeploy x N
RollbackRoute->>DB: deleteAllPromoteRunsForTarget
RollbackRoute->>DB: processOutboxEvents (post-commit)
RollbackRoute-->>Client: 200 result
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant ForkRoute as POST /fork
participant PromoteRoute as POST /fork/promote
participant RollbackRoute as POST /fork/rollback
participant DB as Database (tx)
participant BG as Background
Note over Client, BG: Fork flow
Client->>ForkRoute: "POST /workspaces/{id}/fork"
ForkRoute->>DB: insert workspace + permissions + workflows + resources + mappings
DB-->>ForkRoute: childWorkspaceId
ForkRoute->>BG: schedule forkContentCopy
ForkRoute-->>Client: 201 workspace
Note over Client, DB: Promote flow
Client->>PromoteRoute: "POST /fork/promote {direction, force}"
PromoteRoute->>DB: loadSourceDeployedStates (pre-tx)
PromoteRoute->>DB: tx: acquireTargetLock + edgeLock
PromoteRoute->>DB: computePromotePlan
PromoteRoute->>DB: copyWorkflowStateIntoTarget x N
PromoteRoute->>DB: upsertPromoteRun
DB-->>PromoteRoute: deployTargetIds
loop post-tx deploys
PromoteRoute->>DB: performFullDeploy
end
PromoteRoute-->>Client: 200 report
Note over Client, DB: Rollback flow
Client->>RollbackRoute: POST /fork/rollback
RollbackRoute->>DB: tx: acquireTargetLock + edgeLock
RollbackRoute->>DB: reactivateDeployedVersion / undeploy x N
RollbackRoute->>DB: deleteAllPromoteRunsForTarget
RollbackRoute->>DB: processOutboxEvents (post-commit)
RollbackRoute-->>Client: 200 result
Reviews (3): Last reviewed commit: "update modal state" | Re-trigger Greptile |
|
@greptile |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 055cb47. Configure here.
|
bugbot run |
|
@greptile |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7635ad9. Configure here.
| return | ||
| } | ||
| setIsForkModalOpen(true) | ||
| } |
There was a problem hiding this comment.
Workspace limit blocks rollback
High Severity
handleForkAction treats “Manage Forks” like creating a workspace: when canCreateWorkspace is false it redirects to billing and never opens the modal. Rollback and the durable Activity log live only in that modal, so users at a plan workspace cap can sync but cannot undo or review fork history from the UI.
Reviewed by Cursor Bugbot for commit 7635ad9. Configure here.
| className='mx-2' | ||
| /> | ||
| {activeTab === 'activity' ? ( | ||
| <ForkActivityPanel report={lastReport} pending={submitting} pendingLabel='Syncing…' /> |
There was a problem hiding this comment.
Sync Activity omits audit poll
Medium Severity
The Sync modal’s Activity tab renders ForkActivityPanel without backgroundWorkspaceId, so it never calls useWorkspaceBackgroundWork. Durable fork_sync rows written by the promote API for the current workspace are omitted from the audit table; only the ephemeral in-session lastReport appears after a sync.
Reviewed by Cursor Bugbot for commit 7635ad9. Configure here.
| onSettled: () => { | ||
| queryClient.invalidateQueries({ queryKey: forkKeys.all }) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Activity cache not invalidated
Medium Severity
Fork, promote, and rollback mutations invalidate fork lineage/mapping caches but not backgroundWorkKeys. After those operations complete, the Activity tab can keep stale or empty job lists until staleTime, polling, or refocus—especially right after fork when the modal switches to Activity without an in-session report.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7635ad9. Configure here.


Summary
Be able to Fork Workspaces. And push/pull changes into or from parent workspaces. Allows one click promotion between environments and supports mapping credentials, secrets, and resources.
Type of Change
Testing
Tested manually
Checklist