d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
import { workflow, workflowDeploymentVersion } from '@sim/db/schema'
|
|
import { and, eq } from 'drizzle-orm'
|
|
import type { DbOrTx } from '@/lib/db/types'
|
|
import { enqueueWorkflowDeploymentSideEffects } from '@/lib/workflows/deployment-outbox'
|
|
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
|
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
|
|
|
interface ReactivateDeployedVersionParams {
|
|
tx: DbOrTx
|
|
workflowId: string
|
|
version: number
|
|
userId: string
|
|
requestId: string
|
|
}
|
|
|
|
export interface ReactivateDeployedVersionResult {
|
|
deploymentVersionId: string
|
|
/**
|
|
* Outbox event id enqueued inside the transaction. Process it AFTER the tx commits
|
|
* (or rely on the outbox cron/reaper if the process dies first).
|
|
*/
|
|
outboxEventId: string
|
|
}
|
|
|
|
/**
|
|
* Reactivate a prior deployment version AND restore the workflow's draft to it using
|
|
* ONLY DB writes against the provided transaction, enqueuing the deployment
|
|
* side-effect (webhook / schedule / MCP re-subscription) to the outbox for processing
|
|
* AFTER the tx commits. This composes the DB halves of {@link activateWorkflowVersion}
|
|
* and `performRevertToVersion` so a fork rollback can run atomically under its fork
|
|
* advisory lock - the heavy side-effects never run inside the locked tx.
|
|
*
|
|
* Deliberately does NOT call `assertWorkflowMutable`: a rollback is an admin force-undo
|
|
* and must not be blocked by a workflow/folder lock (that check is also not tx-safe).
|
|
* Idempotent: deactivate-all + activate-target + overwrite-draft yield the same state
|
|
* on retry.
|
|
*
|
|
* Returns null when the target version row no longer exists, so the caller can mark the
|
|
* workflow skipped rather than failing the whole rollback.
|
|
*/
|
|
export async function reactivateDeployedVersionInTx(
|
|
params: ReactivateDeployedVersionParams
|
|
): Promise<ReactivateDeployedVersionResult | null> {
|
|
const { tx, workflowId, version, userId, requestId } = params
|
|
const now = new Date()
|
|
|
|
// Lock the workflow row so this serializes with a concurrent (unlocked) promote
|
|
// deploy loop, which locks the same row in deployWorkflow - guaranteeing the final
|
|
// (active version, draft) pair is always coherent regardless of commit order.
|
|
await tx
|
|
.select({ id: workflow.id })
|
|
.from(workflow)
|
|
.where(eq(workflow.id, workflowId))
|
|
.limit(1)
|
|
.for('update')
|
|
|
|
const [versionRow] = await tx
|
|
.select({ id: workflowDeploymentVersion.id, state: workflowDeploymentVersion.state })
|
|
.from(workflowDeploymentVersion)
|
|
.where(
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflowId),
|
|
eq(workflowDeploymentVersion.version, version)
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
if (!versionRow) return null
|
|
|
|
const deployedState = versionRow.state as {
|
|
blocks?: Record<string, unknown>
|
|
edges?: unknown[]
|
|
loops?: Record<string, unknown>
|
|
parallels?: Record<string, unknown>
|
|
variables?: WorkflowState['variables']
|
|
}
|
|
if (!deployedState.blocks || !deployedState.edges) {
|
|
throw new Error(
|
|
`Deployment version ${version} for workflow ${workflowId} has an invalid state structure`
|
|
)
|
|
}
|
|
|
|
// Activate the target version (deactivate every other), mark the workflow deployed.
|
|
await tx
|
|
.update(workflowDeploymentVersion)
|
|
.set({ isActive: false })
|
|
.where(eq(workflowDeploymentVersion.workflowId, workflowId))
|
|
await tx
|
|
.update(workflowDeploymentVersion)
|
|
.set({ isActive: true })
|
|
.where(
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflowId),
|
|
eq(workflowDeploymentVersion.version, version)
|
|
)
|
|
)
|
|
await tx
|
|
.update(workflow)
|
|
.set({ isDeployed: true, deployedAt: now })
|
|
.where(eq(workflow.id, workflowId))
|
|
|
|
// Restore the draft to the deployed version's state.
|
|
const hasVariables = Object.hasOwn(deployedState, 'variables')
|
|
const restoredState: WorkflowState = {
|
|
blocks: deployedState.blocks,
|
|
edges: deployedState.edges,
|
|
loops: deployedState.loops || {},
|
|
parallels: deployedState.parallels || {},
|
|
lastSaved: now.getTime(),
|
|
} as WorkflowState
|
|
if (hasVariables) {
|
|
restoredState.variables = deployedState.variables || {}
|
|
}
|
|
|
|
const saveResult = await saveWorkflowToNormalizedTables(workflowId, restoredState, tx)
|
|
if (!saveResult.success) {
|
|
throw new Error(saveResult.error || `Failed to restore draft for workflow ${workflowId}`)
|
|
}
|
|
|
|
await tx
|
|
.update(workflow)
|
|
.set({
|
|
...(hasVariables ? { variables: deployedState.variables || {} } : {}),
|
|
lastSynced: now,
|
|
updatedAt: now,
|
|
})
|
|
.where(eq(workflow.id, workflowId))
|
|
|
|
const outboxEventId = await enqueueWorkflowDeploymentSideEffects(tx, {
|
|
workflowId,
|
|
deploymentVersionId: versionRow.id,
|
|
userId,
|
|
requestId,
|
|
forceRecreateSubscriptions: true,
|
|
})
|
|
|
|
return { deploymentVersionId: versionRow.id, outboxEventId }
|
|
}
|