Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

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 }
}