Skip to content

Commit ad7b791

Browse files
improvement(deployments): simplify deployments for chat and indicate active version (#1730)
* improvement(deployment-ux): deployment should indicate and make details configurable when activating previous version * fix activation UI * remove redundant code * revert pulsing dot * fix redeploy bug * bill workspace owner for deployed chat * deployed chat * fix bugs * fix tests, address greptile * fix * ui bug to load api key * fix qdrant fetch tool
1 parent ce4893a commit ad7b791

File tree

19 files changed

+1428
-1102
lines changed

19 files changed

+1428
-1102
lines changed

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { chat, workflow } from '@sim/db/schema'
2+
import { chat, workflow, workspace } from '@sim/db/schema'
33
import { eq } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { createLogger } from '@/lib/logs/console/logger'
@@ -94,11 +94,12 @@ export async function POST(
9494
return addCorsHeaders(createErrorResponse('No input provided', 400), request)
9595
}
9696

97-
// Get the workflow for this chat
97+
// Get the workflow and workspace owner for this chat
9898
const workflowResult = await db
9999
.select({
100100
isDeployed: workflow.isDeployed,
101101
workspaceId: workflow.workspaceId,
102+
variables: workflow.variables,
102103
})
103104
.from(workflow)
104105
.where(eq(workflow.id, deployment.workflowId))
@@ -109,6 +110,22 @@ export async function POST(
109110
return addCorsHeaders(createErrorResponse('Chat workflow is not available', 503), request)
110111
}
111112

113+
let workspaceOwnerId = deployment.userId
114+
if (workflowResult[0].workspaceId) {
115+
const workspaceData = await db
116+
.select({ ownerId: workspace.ownerId })
117+
.from(workspace)
118+
.where(eq(workspace.id, workflowResult[0].workspaceId))
119+
.limit(1)
120+
121+
if (workspaceData.length === 0) {
122+
logger.error(`[${requestId}] Workspace not found for workflow ${deployment.workflowId}`)
123+
return addCorsHeaders(createErrorResponse('Workspace not found', 500), request)
124+
}
125+
126+
workspaceOwnerId = workspaceData[0].ownerId
127+
}
128+
112129
try {
113130
const selectedOutputs: string[] = []
114131
if (deployment.outputConfigs && Array.isArray(deployment.outputConfigs)) {
@@ -145,16 +162,19 @@ export async function POST(
145162
}
146163
}
147164

165+
const workflowForExecution = {
166+
id: deployment.workflowId,
167+
userId: deployment.userId,
168+
workspaceId: workflowResult[0].workspaceId,
169+
isDeployed: true,
170+
variables: workflowResult[0].variables || {},
171+
}
172+
148173
const stream = await createStreamingResponse({
149174
requestId,
150-
workflow: {
151-
id: deployment.workflowId,
152-
userId: deployment.userId,
153-
workspaceId: workflowResult[0].workspaceId,
154-
isDeployed: true,
155-
},
175+
workflow: workflowForExecution,
156176
input: workflowInput,
157-
executingUserId: deployment.userId,
177+
executingUserId: workspaceOwnerId,
158178
streamConfig: {
159179
selectedOutputs,
160180
isSecureMode: true,

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isDev } from '@/lib/environment'
88
import { createLogger } from '@/lib/logs/console/logger'
99
import { getEmailDomain } from '@/lib/urls/utils'
1010
import { encryptSecret } from '@/lib/utils'
11+
import { deployWorkflow } from '@/lib/workflows/db-helpers'
1112
import { checkChatAccess } from '@/app/api/chat/utils'
1213
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
1314

@@ -134,6 +135,22 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
134135
}
135136
}
136137

138+
// Redeploy the workflow to ensure latest version is active
139+
const deployResult = await deployWorkflow({
140+
workflowId: existingChat[0].workflowId,
141+
deployedBy: session.user.id,
142+
})
143+
144+
if (!deployResult.success) {
145+
logger.warn(
146+
`Failed to redeploy workflow for chat update: ${deployResult.error}, continuing with chat update`
147+
)
148+
} else {
149+
logger.info(
150+
`Redeployed workflow ${existingChat[0].workflowId} for chat update (v${deployResult.version})`
151+
)
152+
}
153+
137154
let encryptedPassword
138155

139156
if (password) {

apps/sim/app/api/chat/route.test.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('Chat API Route', () => {
1919
const mockCreateErrorResponse = vi.fn()
2020
const mockEncryptSecret = vi.fn()
2121
const mockCheckWorkflowAccessForChatCreation = vi.fn()
22+
const mockDeployWorkflow = vi.fn()
2223

2324
beforeEach(() => {
2425
vi.resetModules()
@@ -76,6 +77,14 @@ describe('Chat API Route', () => {
7677
vi.doMock('@/app/api/chat/utils', () => ({
7778
checkWorkflowAccessForChatCreation: mockCheckWorkflowAccessForChatCreation,
7879
}))
80+
81+
vi.doMock('@/lib/workflows/db-helpers', () => ({
82+
deployWorkflow: mockDeployWorkflow.mockResolvedValue({
83+
success: true,
84+
version: 1,
85+
deployedAt: new Date(),
86+
}),
87+
}))
7988
})
8089

8190
afterEach(() => {
@@ -236,7 +245,7 @@ describe('Chat API Route', () => {
236245
it('should allow chat deployment when user owns workflow directly', async () => {
237246
vi.doMock('@/lib/auth', () => ({
238247
getSession: vi.fn().mockResolvedValue({
239-
user: { id: 'user-id' },
248+
user: { id: 'user-id', email: '[email protected]' },
240249
}),
241250
}))
242251

@@ -283,7 +292,7 @@ describe('Chat API Route', () => {
283292
it('should allow chat deployment when user has workspace admin permission', async () => {
284293
vi.doMock('@/lib/auth', () => ({
285294
getSession: vi.fn().mockResolvedValue({
286-
user: { id: 'user-id' },
295+
user: { id: 'user-id', email: '[email protected]' },
287296
}),
288297
}))
289298

@@ -393,10 +402,10 @@ describe('Chat API Route', () => {
393402
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
394403
})
395404

396-
it('should reject if workflow is not deployed', async () => {
405+
it('should auto-deploy workflow if not already deployed', async () => {
397406
vi.doMock('@/lib/auth', () => ({
398407
getSession: vi.fn().mockResolvedValue({
399-
user: { id: 'user-id' },
408+
user: { id: 'user-id', email: '[email protected]' },
400409
}),
401410
}))
402411

@@ -415,6 +424,7 @@ describe('Chat API Route', () => {
415424
hasAccess: true,
416425
workflow: { userId: 'user-id', workspaceId: null, isDeployed: false },
417426
})
427+
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
418428

419429
const req = new NextRequest('http://localhost:3000/api/chat', {
420430
method: 'POST',
@@ -423,11 +433,11 @@ describe('Chat API Route', () => {
423433
const { POST } = await import('@/app/api/chat/route')
424434
const response = await POST(req)
425435

426-
expect(response.status).toBe(400)
427-
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
428-
'Workflow must be deployed before creating a chat',
429-
400
430-
)
436+
expect(response.status).toBe(200)
437+
expect(mockDeployWorkflow).toHaveBeenCalledWith({
438+
workflowId: 'workflow-123',
439+
deployedBy: 'user-id',
440+
})
431441
})
432442
})
433443
})

apps/sim/app/api/chat/route.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isDev } from '@/lib/environment'
99
import { createLogger } from '@/lib/logs/console/logger'
1010
import { getBaseUrl } from '@/lib/urls/utils'
1111
import { encryptSecret } from '@/lib/utils'
12+
import { deployWorkflow } from '@/lib/workflows/db-helpers'
1213
import { checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
1314
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
1415

@@ -126,11 +127,20 @@ export async function POST(request: NextRequest) {
126127
return createErrorResponse('Workflow not found or access denied', 404)
127128
}
128129

129-
// Verify the workflow is deployed (required for chat deployment)
130-
if (!workflowRecord.isDeployed) {
131-
return createErrorResponse('Workflow must be deployed before creating a chat', 400)
130+
// Always deploy/redeploy the workflow to ensure latest version
131+
const result = await deployWorkflow({
132+
workflowId,
133+
deployedBy: session.user.id,
134+
})
135+
136+
if (!result.success) {
137+
return createErrorResponse(result.error || 'Failed to deploy workflow', 500)
132138
}
133139

140+
logger.info(
141+
`${workflowRecord.isDeployed ? 'Redeployed' : 'Auto-deployed'} workflow ${workflowId} for chat (v${result.version})`
142+
)
143+
134144
// Encrypt password if provided
135145
let encryptedPassword = null
136146
if (authType === 'password' && password) {

apps/sim/app/api/workflows/[id]/deploy/route.ts

Lines changed: 14 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { apiKey, db, workflow, workflowDeploymentVersion } from '@sim/db'
2-
import { and, desc, eq, sql } from 'drizzle-orm'
2+
import { and, desc, eq } from 'drizzle-orm'
33
import { type NextRequest, NextResponse } from 'next/server'
4-
import { v4 as uuidv4 } from 'uuid'
54
import { createLogger } from '@/lib/logs/console/logger'
65
import { generateRequestId } from '@/lib/utils'
7-
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
6+
import { deployWorkflow } from '@/lib/workflows/db-helpers'
87
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
98
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
109

@@ -138,37 +137,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
138137
}
139138
} catch (_err) {}
140139

141-
logger.debug(`[${requestId}] Getting current workflow state for deployment`)
142-
143-
const normalizedData = await loadWorkflowFromNormalizedTables(id)
144-
145-
if (!normalizedData) {
146-
logger.error(`[${requestId}] Failed to load workflow from normalized tables`)
147-
return createErrorResponse('Failed to load workflow state', 500)
148-
}
149-
150-
const currentState = {
151-
blocks: normalizedData.blocks,
152-
edges: normalizedData.edges,
153-
loops: normalizedData.loops,
154-
parallels: normalizedData.parallels,
155-
lastSaved: Date.now(),
156-
}
157-
158-
logger.debug(`[${requestId}] Current state retrieved from normalized tables:`, {
159-
blocksCount: Object.keys(currentState.blocks).length,
160-
edgesCount: currentState.edges.length,
161-
loopsCount: Object.keys(currentState.loops).length,
162-
parallelsCount: Object.keys(currentState.parallels).length,
163-
})
164-
165-
if (!currentState || !currentState.blocks) {
166-
logger.error(`[${requestId}] Invalid workflow state retrieved`, { currentState })
167-
throw new Error('Invalid workflow state: missing blocks')
168-
}
169-
170-
const deployedAt = new Date()
171-
logger.debug(`[${requestId}] Proceeding with deployment at ${deployedAt.toISOString()}`)
140+
logger.debug(`[${requestId}] Validating API key for deployment`)
172141

173142
let keyInfo: { name: string; type: 'personal' | 'workspace' } | null = null
174143
let matchedKey: {
@@ -260,45 +229,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
260229
return createErrorResponse('Unable to determine deploying user', 400)
261230
}
262231

263-
await db.transaction(async (tx) => {
264-
const [{ maxVersion }] = await tx
265-
.select({ maxVersion: sql`COALESCE(MAX("version"), 0)` })
266-
.from(workflowDeploymentVersion)
267-
.where(eq(workflowDeploymentVersion.workflowId, id))
268-
269-
const nextVersion = Number(maxVersion) + 1
270-
271-
await tx
272-
.update(workflowDeploymentVersion)
273-
.set({ isActive: false })
274-
.where(
275-
and(
276-
eq(workflowDeploymentVersion.workflowId, id),
277-
eq(workflowDeploymentVersion.isActive, true)
278-
)
279-
)
280-
281-
await tx.insert(workflowDeploymentVersion).values({
282-
id: uuidv4(),
283-
workflowId: id,
284-
version: nextVersion,
285-
state: currentState,
286-
isActive: true,
287-
createdAt: deployedAt,
288-
createdBy: actorUserId,
289-
})
232+
const deployResult = await deployWorkflow({
233+
workflowId: id,
234+
deployedBy: actorUserId,
235+
pinnedApiKeyId: matchedKey?.id,
236+
includeDeployedState: true,
237+
workflowName: workflowData!.name,
238+
})
290239

291-
const updateData: Record<string, unknown> = {
292-
isDeployed: true,
293-
deployedAt,
294-
deployedState: currentState,
295-
}
296-
if (providedApiKey && matchedKey) {
297-
updateData.pinnedApiKeyId = matchedKey.id
298-
}
240+
if (!deployResult.success) {
241+
return createErrorResponse(deployResult.error || 'Failed to deploy workflow', 500)
242+
}
299243

300-
await tx.update(workflow).set(updateData).where(eq(workflow.id, id))
301-
})
244+
const deployedAt = deployResult.deployedAt!
302245

303246
if (matchedKey) {
304247
try {
@@ -313,31 +256,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
313256

314257
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
315258

316-
// Track workflow deployment
317-
try {
318-
const { trackPlatformEvent } = await import('@/lib/telemetry/tracer')
319-
320-
// Aggregate block types to understand which blocks are being used
321-
const blockTypeCounts: Record<string, number> = {}
322-
for (const block of Object.values(currentState.blocks)) {
323-
const blockType = (block as any).type || 'unknown'
324-
blockTypeCounts[blockType] = (blockTypeCounts[blockType] || 0) + 1
325-
}
326-
327-
trackPlatformEvent('platform.workflow.deployed', {
328-
'workflow.id': id,
329-
'workflow.name': workflowData!.name,
330-
'workflow.blocks_count': Object.keys(currentState.blocks).length,
331-
'workflow.edges_count': currentState.edges.length,
332-
'workflow.has_loops': Object.keys(currentState.loops).length > 0,
333-
'workflow.has_parallels': Object.keys(currentState.parallels).length > 0,
334-
'workflow.api_key_type': keyInfo?.type || 'default',
335-
'workflow.block_types': JSON.stringify(blockTypeCounts),
336-
})
337-
} catch (_e) {
338-
// Silently fail
339-
}
340-
341259
const responseApiKeyInfo = keyInfo ? `${keyInfo.name} (${keyInfo.type})` : 'Default key'
342260

343261
return createSuccessResponse({

0 commit comments

Comments
 (0)