Skip to content

feat(workspaces): fork + push/pull#5210

Open
icecrasher321 wants to merge 10 commits into
stagingfrom
feat/ws-fork
Open

feat(workspaces): fork + push/pull#5210
icecrasher321 wants to merge 10 commits into
stagingfrom
feat/ws-fork

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

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

  • New feature

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 25, 2026 10:35pm

Request Review

@icecrasher321 icecrasher321 marked this pull request as ready for review June 25, 2026 21:38
@cursor

cursor Bot commented Jun 25, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Mutates workspaces, deployed workflows, and secret mappings across environments; promote/rollback are destructive and auth relies on admin gates plus mapping validation.

Overview
Adds Enterprise-gated workspace forking: create a child workspace from a parent (copying deployed workflows and optionally files, tables, knowledge bases, tools, skills, and MCP configs), with heavy row/blob copy deferred to a background job.

Sync (push/pull) along the direct parent–child edge lets admins map credentials, env vars, and other resources, preview workflow create/update/archive changes (including drift), then promote deployed state into the target—with optional force on drift and rollback of the last promote. New APIs cover fork, lineage, resources, mapping, diff, promote, and rollback; the sidebar exposes fork, sync, and manage-forks modals when forking is available.

Workflow copy/promote logic is centralized with deterministic block IDs, resource remapping, and stricter cross-workspace handling (unmapped workflow references cleared). forkedFromWorkspaceId and a persistent edge resource map back identity and mappings; audit events record fork, promote, and rollback.

Reviewed by Cursor Bugbot for commit 2739547. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2739547. Configure here.

Comment thread apps/sim/lib/workspaces/fork/promote/promote-run-store.ts
Comment thread apps/sim/hooks/queries/workspace-fork.ts
Comment thread apps/sim/lib/workspaces/fork/promote/rollback.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds workspace forking — creating a child workspace that inherits the parent's deployed workflows — along with push/pull promote and one-level rollback. The implementation covers DB schema, API routes, a credential-propagation path, deterministic block-ID remapping for stable webhook URLs, and background copy of heavy content (table rows, KB embeddings) via Trigger.dev.

  • Fork creation reads deployed workflow states before the transaction to avoid cross-connection deadlocks, copies selected resources (custom tools, skills, MCP configs, tables, KBs) with fresh IDs, strips OAuth secrets, and seeds identity mappings for future promotes.
  • Promote/rollback serializes concurrent operations with a pg advisory lock; the plan is computed inside the transaction, deployed outside it, and an undo snapshot is stored so the last promote can be reversed.
  • Mapping editor (GET/PUT /fork/mapping) allows credentials, env vars, and other references to be manually pointed at target-workspace equivalents before a promote runs.

Confidence Score: 3/5

The core fork/promote/rollback logic is well-structured and the auth boundaries are correct, but the mapping save path can issue thousands of individual DB round-trips in a single transaction — a real timeout risk at the contract's allowed input size.

Two findings affect the mapping update path in a compounding way: applyForkMappingEntries calls upsertEdgeMappings and deleteEdgeMappingByChildResource once per entry (up to 5,000), and the delete queries scan without a child_resource_id index. Either alone would be a latency concern; together they can exhaust the transaction timeout or the connection pool under realistic load. Additionally, workflow-type entries bypass target validation in validateForkMappingTargets, allowing arbitrary workflow identity rows to be inserted through the mapping editor. These issues are all confined to the mapping-save code path; the promote, rollback, and fork-creation flows read cleanly and the schema migration is careful.

apps/sim/lib/workspaces/fork/mapping/mapping-service.ts and apps/sim/lib/workspaces/fork/mapping/mapping-store.ts for the serial-query pattern; packages/db/migrations/0250_workspace_forking.sql for the missing child_resource_id index; apps/sim/lib/workspaces/fork/copy/copy-resources.ts for the connectorId on KB copy.

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/fork/mapping/mapping-service.ts Two issues: serial per-entry DB queries in applyForkMappingEntries (up to 10k ops at contract limit), and workflow-type entries bypass validateForkMappingTargets allowing arbitrary identity rows to be inserted via the PUT mapping endpoint.
packages/db/migrations/0250_workspace_forking.sql Adds tables and indexes for fork resource maps and promote runs; CONCURRENTLY index for the existing workspace table is handled correctly, but child_resource_id is not indexed — causing O(N×M) scans in deleteEdgeMappingByChildResource when called in a loop.
apps/sim/lib/workspaces/fork/copy/copy-resources.ts Copies resource container rows for fork; correctly strips MCP OAuth secrets and document connectorIds, but the knowledge-base container itself retains the parent workspace's connectorId reference.
apps/sim/lib/workspaces/fork/promote/promote.ts Core promote orchestrator: acquires advisory lock, computes plan inside transaction, propagates credential membership, saves undo snapshot, then deploys outside the transaction. Logic is sound; blocked results return 200 which clients must check.
apps/sim/lib/workspaces/fork/promote/rollback.ts Rollback correctly reactivates prior versions before cleaning up, holds the edge lock during cleanup, and guards against concurrent promotes superseding the undo point. Authorization relies on admin-on-target which is correct for rollback semantics.
apps/sim/lib/workspaces/fork/lineage/authz.ts Enterprise/self-hosted gate logic is correct: neither-flag → 404, billing-enabled → enterprise check, forking-flag-only → allow. assertCanPromote enforces read-on-source + admin-on-target which matches the destructive nature of a promote.
apps/sim/lib/workspaces/fork/create-fork.ts Fork creation reads deployed states before the transaction to avoid cross-connection issues, correctly copies permissions from the source, and defers heavy content (table rows, KB embeddings) to a background job via Trigger.dev or runDetached.
apps/sim/lib/workspaces/fork/mapping/mapping-store.ts seedEdgeMappings correctly chunks batch inserts, but upsertEdgeMappings loops one INSERT per entry — compounding the serial-query problem when called in a loop from applyForkMappingEntries.
packages/db/schema.ts Adds workspace_fork_resource_map and workspace_fork_promote_run tables with correct FK/cascade semantics; sourceWorkspaceId/targetWorkspaceId on promote_run intentionally lack FKs (soft-reference for undo history).
apps/sim/lib/workspaces/fork/promote/promote-plan.ts Plan computation is read-only and shared between diff preview and promote; correctly guards against archiving target-native workflows and uses updatedAt-based drift detection which is conservative but correct.
apps/sim/app/api/workspaces/[id]/fork/promote/route.ts Promote route correctly checks auth, delegates to promoteFork, and returns HTTP 200 for blocked promotes — clients must inspect the blocked field to distinguish success from a blocked state.
apps/sim/lib/workspaces/fork/copy/copy-workflows.ts Correctly uses deriveForkBlockId for deterministic block IDs across promotes, strips trigger-runtime subblocks, and clears cross-workspace workflow references with clearUnmapped:true.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Browser UI
    participant API as API Route
    participant Auth as authz.ts
    participant Plan as promote-plan.ts
    participant DB as PostgreSQL

    UI->>API: POST /fork (create fork)
    API->>Auth: assertCanFork(sourceId, userId)
    Auth->>DB: checkWorkspaceAccess + plan policy
    API->>DB: tx: insert workspace, permissions, copy workflows/resources, seedMappings
    API-->>UI: "{ workspace, workflowsCopied }"

    UI->>API: "GET /fork/diff?direction=push"
    API->>Auth: assertCanPromote(id, otherId, direction, userId)
    API->>Plan: computeForkPromotePlan(edge, source, target)
    Plan->>DB: getEdgeMappingRows, listDeployedWorkflows, readDeployedState
    API-->>UI: "{ willUpdate, willCreate, willArchive, unmappedRequired, drift }"

    UI->>API: PUT /fork/mapping (save credential/env mappings)
    API->>Auth: assertCanPromote
    API->>DB: validateForkMappingTargets, tx: applyForkMappingEntries

    UI->>API: POST /fork/promote
    API->>Auth: assertCanPromote
    API->>DB: tx: acquireEdgeLock, computePlan, copyWorkflows, upsertMappings, upsertPromoteRun
    API->>DB: performFullDeploy (outside tx, per workflow)
    API-->>UI: "{ promoteRunId, updated, created, archived, redeployed }"

    UI->>API: POST /fork/rollback
    API->>Auth: assertCanRollback (admin on target)
    API->>DB: resolveForkEdge, getPromoteRunForRollback
    API->>DB: performActivateVersion (prior versions)
    API->>DB: tx: acquireEdgeLock, undeploy/archive created, deleteIdentityRows, deletePromoteRun
    API-->>UI: "{ restored, archived, unarchived }"
Loading
%%{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 UI as Browser UI
    participant API as API Route
    participant Auth as authz.ts
    participant Plan as promote-plan.ts
    participant DB as PostgreSQL

    UI->>API: POST /fork (create fork)
    API->>Auth: assertCanFork(sourceId, userId)
    Auth->>DB: checkWorkspaceAccess + plan policy
    API->>DB: tx: insert workspace, permissions, copy workflows/resources, seedMappings
    API-->>UI: "{ workspace, workflowsCopied }"

    UI->>API: "GET /fork/diff?direction=push"
    API->>Auth: assertCanPromote(id, otherId, direction, userId)
    API->>Plan: computeForkPromotePlan(edge, source, target)
    Plan->>DB: getEdgeMappingRows, listDeployedWorkflows, readDeployedState
    API-->>UI: "{ willUpdate, willCreate, willArchive, unmappedRequired, drift }"

    UI->>API: PUT /fork/mapping (save credential/env mappings)
    API->>Auth: assertCanPromote
    API->>DB: validateForkMappingTargets, tx: applyForkMappingEntries

    UI->>API: POST /fork/promote
    API->>Auth: assertCanPromote
    API->>DB: tx: acquireEdgeLock, computePlan, copyWorkflows, upsertMappings, upsertPromoteRun
    API->>DB: performFullDeploy (outside tx, per workflow)
    API-->>UI: "{ promoteRunId, updated, created, archived, redeployed }"

    UI->>API: POST /fork/rollback
    API->>Auth: assertCanRollback (admin on target)
    API->>DB: resolveForkEdge, getPromoteRunForRollback
    API->>DB: performActivateVersion (prior versions)
    API->>DB: tx: acquireEdgeLock, undeploy/archive created, deleteIdentityRows, deletePromoteRun
    API-->>UI: "{ restored, archived, unarchived }"
Loading

Reviews (1): Last reviewed commit: "make rollback part of the footer" | Re-trigger Greptile

Comment thread apps/sim/lib/workspaces/fork/mapping/mapping-service.ts Outdated
Comment thread packages/db/migrations/0250_workspace_forking.sql
Comment thread apps/sim/lib/workspaces/fork/mapping/mapping-service.ts Outdated
Comment thread apps/sim/lib/workspaces/fork/copy/copy-resources.ts
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

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