Skip to content

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

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

feat(workspaces): fork + push/pull#5210
icecrasher321 wants to merge 36 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 28, 2026 1:49am

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
Promote/rollback rewrite deployed workflows, mappings, and deployments at scale with complex resource remapping; incorrect mapping or partial deploy failures could leave targets misconfigured until users re-sync or redeploy.

Overview
Introduces workspace forking and parent/child sync (push/pull) as an Enterprise feature (or self-hosted via FORKING_ENABLED), with workspace-admin-only API routes for creating forks, viewing lineage, editing resource mappings, previewing diffs, promoting workflows, rolling back the last promote, and listing background activity.

The outbox processor cron now also reaps stale fork background-work rows stuck in processing so crashed workers do not leave the Activity UI spinning forever.

Manage Forks and Sync workspace modals in the sidebar expose fork creation (optional resource copy), mapping wizard with dependent-field reconfiguration, overwrite confirmation, rollback, and an Activity tab backed by background_work_status polling. Heavy post-fork content copy runs via a Trigger.dev task (or inline runner) with audit rows recorded on the source workspace.

Workflow copy/sync logic gains shared subblock remapping (variables, conditions, cross-workflow references with optional clearUnmapped for cross-workspace), and deploy now refuses archived workflows under row lock to stay consistent with fork rollback archiving.

A small editor fix wraps tool-input params in DependencyBlockTypeProvider so dependsOn selectors resolve against the nested tool’s block config. ChipModalFooter adds optional hideCancel and primaryAdjacentAction for multi-step sync footers.

Reviewed by Cursor Bugbot for commit d5b4bd5. Configure here.

Comment thread apps/sim/lib/workspaces/fork/promote/promote-run-store.ts Outdated
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 introduces workspace forking — the ability to create a child workspace from a parent and bi-directionally sync (push/pull) deployed workflows and mapped resources between them. It is a large, well-structured addition spanning new DB tables, advisory-locked promote/rollback transactions, a credential-access propagation layer, and a full set of UI components.

  • Fork creation snapshots the parent's deployed workflows and resource containers into a child workspace inside a bounded, lock-guarded transaction; heavy content (table rows, KB documents, files) is deferred to a background job backed by Trigger.dev or runDetached.
  • Promote (push/pull) computes a diff plan, resolves resource mappings, force-replaces the target's deployed state, propagates credential member access, and captures a single-level rollback snapshot — all inside a pair of advisory locks (fork-target acquired before fork-edge) that serialize concurrent syncs and prevent interleaving with the rollback path.
  • Rollback re-reads and re-confirms the undo point under the same locks before performing any write, making it strictly atomic and idempotent; archived workflows are un-archived and reactivated, created workflows are archived, and undeploy side-effects are enqueued to the durable outbox.

Confidence Score: 5/5

Safe to merge — the promote and rollback paths are carefully locked and the only new finding is a performance gap in the mapping GET endpoint.

The core transaction paths (promote, rollback, fork creation) are well-guarded with advisory locks, deterministic lock ordering, locked re-confirmation of undo points, and durable outbox side-effects. The sole new finding is that getForkMappingView reads deployed workflow states one at a time in a sequential loop with no workflow count cap, while the sibling diff and promote paths already use bounded-concurrency batching and throw at 1000 workflows. This is a latency concern on the GET /mapping endpoint for large workspaces, not a correctness issue.

apps/sim/lib/workspaces/fork/mapping/mapping-service.ts — the getForkMappingView sequential-read loop (lines 94–107) should adopt the same bounded-concurrency and count-cap pattern used by loadSourceDeployedStates.

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/fork/mapping/mapping-service.ts Core mapping service: view + apply + validate. Sequential workflow state reads in getForkMappingView lack the bounded-concurrency and count cap that loadSourceDeployedStates uses in the promote/diff paths.
apps/sim/lib/workspaces/fork/promote/promote.ts Main promote orchestrator: advisory-locked transaction, credential-access propagation, per-target deploy loop, and outbox-backed side-effects. Solid design with good TOCTOU guards inside the lock.
apps/sim/lib/workspaces/fork/promote/rollback.ts Atomic single-level undo: advisory-locked re-confirmation of the run inside the transaction, deterministic lock ordering, and durable outbox events for side-effects after commit. Well-guarded against interleaving concurrent promotes.
apps/sim/lib/workspaces/fork/lineage/authz.ts Enterprise-gated fork authorization: admin-on-both checks for promote, admin-on-target for rollback. Correctly rejects workflow-type entries in validateForkMappingTargets (addressing the previous review concern).
packages/db/migrations/0250_workspace_forking.sql Adds fork resource map and promote-run tables with idempotent DDL. Uses CONCURRENTLY index on the existing workspace table to avoid table-lock. Missing an index on child_resource_id (flagged in a prior review thread).
packages/db/migrations/0251_wakeful_smiling_tiger.sql Adds background_work_status, workspace_fork_block_map, and workspace_fork_dependent_value tables with appropriate FK cascades and covering indexes.
apps/sim/lib/workspaces/fork/create-fork.ts Fork creation: seeded identity mappings, bounded workflow copy inside a lock, post-commit background content copy via Trigger.dev or runDetached. Error handling correctly isolates background-scheduling failures from the already-committed fork.
apps/sim/lib/workspaces/fork/mapping/mapping-store.ts Batch delete (deleteEdgeMappingsByChildResources) and chunked upsert (upsertEdgeMappings at 1000 rows/batch) for mapping entries — addresses the serial per-entry pattern called out in a prior review thread.
apps/sim/app/api/workspaces/[id]/fork/promote/route.ts Promote route: session auth, contract parsing, assertCanPromote (admin on both sides), promoteFork, audit log, and background-work record. Audit emitted only on success (blocked result short-circuits before recordAudit).
apps/sim/app/api/workspaces/[id]/fork/rollback/route.ts Rollback route: session auth, assertCanRollback (admin on target only), rollback execution, fire-and-forget audit, and background-work record.

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 Auth as assertCanPromote/Fork
    participant DB as PostgreSQL (advisory locks)
    participant Outbox as Deployment Outbox

    Client->>ForkRoute: fork workspace
    ForkRoute->>Auth: assertCanFork (admin + plan gate)
    Auth-->>ForkRoute: source + policy
    ForkRoute->>DB: tx(lock) — copy workflows, seed mappings
    DB-->>ForkRoute: new workspace id
    ForkRoute->>Outbox: schedule background content copy
    ForkRoute-->>Client: 201 workspaceId

    Client->>PromoteRoute: push/pull with dependentValues
    PromoteRoute->>Auth: assertCanPromote (admin on both)
    Auth->>DB: resolveForkEdge (2 queries)
    PromoteRoute->>DB: loadSourceDeployedStates (pre-tx)
    PromoteRoute->>DB: tx(acquireTargetLock + acquireEdgeLock)
    DB->>DB: computePromotePlan
    DB->>DB: copyWorkflowStateIntoTarget (each item)
    DB->>DB: reconcileBlockPairs + upsertIdentityMappings
    DB->>DB: propagateCredentialAccess
    DB->>DB: upsertPromoteRun (snapshot for rollback)
    DB-->>PromoteRoute: promoteRunId + counts
    PromoteRoute->>Outbox: process undeploy events (post-commit)
    PromoteRoute->>DB: performFullDeploy (per target, unlocked)
    PromoteRoute-->>Client: updated/created/archived/redeployed

    Client->>RollbackRoute: rollback (otherWorkspaceId)
    RollbackRoute->>Auth: assertCanRollback (admin on target)
    RollbackRoute->>DB: getLatestPromoteRunForTarget (pre-tx)
    RollbackRoute->>DB: tx(acquireTargetLock + acquireEdgeLock)
    DB->>DB: re-confirm run is still newest
    DB->>DB: un-archive + reactivate / undeploy + archive
    DB->>DB: deleteAllPromoteRunsForTarget
    DB-->>RollbackRoute: outbox event ids
    RollbackRoute->>Outbox: process side-effects (post-commit)
    RollbackRoute-->>Client: restored/archived/unarchived/skipped
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 Client
    participant ForkRoute as POST /fork
    participant PromoteRoute as POST /fork/promote
    participant RollbackRoute as POST /fork/rollback
    participant Auth as assertCanPromote/Fork
    participant DB as PostgreSQL (advisory locks)
    participant Outbox as Deployment Outbox

    Client->>ForkRoute: fork workspace
    ForkRoute->>Auth: assertCanFork (admin + plan gate)
    Auth-->>ForkRoute: source + policy
    ForkRoute->>DB: tx(lock) — copy workflows, seed mappings
    DB-->>ForkRoute: new workspace id
    ForkRoute->>Outbox: schedule background content copy
    ForkRoute-->>Client: 201 workspaceId

    Client->>PromoteRoute: push/pull with dependentValues
    PromoteRoute->>Auth: assertCanPromote (admin on both)
    Auth->>DB: resolveForkEdge (2 queries)
    PromoteRoute->>DB: loadSourceDeployedStates (pre-tx)
    PromoteRoute->>DB: tx(acquireTargetLock + acquireEdgeLock)
    DB->>DB: computePromotePlan
    DB->>DB: copyWorkflowStateIntoTarget (each item)
    DB->>DB: reconcileBlockPairs + upsertIdentityMappings
    DB->>DB: propagateCredentialAccess
    DB->>DB: upsertPromoteRun (snapshot for rollback)
    DB-->>PromoteRoute: promoteRunId + counts
    PromoteRoute->>Outbox: process undeploy events (post-commit)
    PromoteRoute->>DB: performFullDeploy (per target, unlocked)
    PromoteRoute-->>Client: updated/created/archived/redeployed

    Client->>RollbackRoute: rollback (otherWorkspaceId)
    RollbackRoute->>Auth: assertCanRollback (admin on target)
    RollbackRoute->>DB: getLatestPromoteRunForTarget (pre-tx)
    RollbackRoute->>DB: tx(acquireTargetLock + acquireEdgeLock)
    DB->>DB: re-confirm run is still newest
    DB->>DB: un-archive + reactivate / undeploy + archive
    DB->>DB: deleteAllPromoteRunsForTarget
    DB-->>RollbackRoute: outbox event ids
    RollbackRoute->>Outbox: process side-effects (post-commit)
    RollbackRoute-->>Client: restored/archived/unarchived/skipped
Loading

Reviews (6): Last reviewed commit: "fix required + dependsOn combo" | 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 Outdated
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@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.

✅ 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 304a7bb. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/lib/workspaces/fork/background-work/store.ts
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread apps/sim/app/api/workspaces/[id]/background-work/route.ts
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@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.

✅ 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 d5b4bd5. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

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