(params: {
workflowId: string
deployedBy: string
workflowName?: string
/** Optional human-readable summary of what changed, stored on the deployment version. */
description?: string | null
/** Optional human-readable name/label for the deployment version. */
name?: string | null
workflowState?: WorkflowState
validateWorkflowState?: (
workflowState: WorkflowState,
executor: DbOrTx
) => DeployWorkflowValidationResult | Promise<DeployWorkflowValidationResult>
onDeployTransaction?: (
tx: DbOrTx,
result: { deploymentVersionId: string; version: number; previousVersionId?: string }
) => Promise<void>
})
| 596 | } |
| 597 | |
| 598 | export async function deployWorkflow(params: { |
| 599 | workflowId: string |
| 600 | deployedBy: string |
| 601 | workflowName?: string |
| 602 | /** Optional human-readable summary of what changed, stored on the deployment version. */ |
| 603 | description?: string | null |
| 604 | /** Optional human-readable name/label for the deployment version. */ |
| 605 | name?: string | null |
| 606 | workflowState?: WorkflowState |
| 607 | validateWorkflowState?: ( |
| 608 | workflowState: WorkflowState, |
| 609 | executor: DbOrTx |
| 610 | ) => DeployWorkflowValidationResult | Promise<DeployWorkflowValidationResult> |
| 611 | onDeployTransaction?: ( |
| 612 | tx: DbOrTx, |
| 613 | result: { deploymentVersionId: string; version: number; previousVersionId?: string } |
| 614 | ) => Promise<void> |
| 615 | }): Promise<{ |
| 616 | success: boolean |
| 617 | version?: number |
| 618 | deploymentVersionId?: string |
| 619 | deployedAt?: Date |
| 620 | previousVersionId?: string |
| 621 | currentState?: WorkflowState |
| 622 | error?: string |
| 623 | errorCode?: 'validation' | 'not_found' |
| 624 | }> { |
| 625 | const { workflowId, deployedBy, workflowName } = params |
| 626 | |
| 627 | try { |
| 628 | const now = new Date() |
| 629 | let currentState: WorkflowState | null = null |
| 630 | |
| 631 | const deployedVersion = await db.transaction(async (tx) => { |
| 632 | if (!(await lockWorkflowForUpdate(tx, workflowId))) { |
| 633 | return { |
| 634 | success: false as const, |
| 635 | error: 'Workflow not found', |
| 636 | errorCode: 'not_found' as const, |
| 637 | } |
| 638 | } |
| 639 | |
| 640 | // Refuse to deploy an archived (soft-deleted) workflow. Checked under the row |
| 641 | // lock so it's atomic with a concurrent fork rollback that archives a |
| 642 | // promote-created workflow: a stale promote deploy can never resurrect it into |
| 643 | // an archived-but-deployed (incoherent) state. |
| 644 | const [archivedRow] = await tx |
| 645 | .select({ archivedAt: workflow.archivedAt }) |
| 646 | .from(workflow) |
| 647 | .where(eq(workflow.id, workflowId)) |
| 648 | .limit(1) |
| 649 | if (archivedRow?.archivedAt != null) { |
| 650 | return { |
| 651 | success: false as const, |
| 652 | error: 'Cannot deploy an archived workflow', |
| 653 | errorCode: 'validation' as const, |
| 654 | } |
| 655 | } |
no test coverage detected