/** * POST /api/workflows/deploy * Deploy workflow to n8n via REST API * Worker handles this directly (mostly external API calls, minimal CPU) * * Pipeline gate: Validate → Simulate → Deploy * Deployment ≠ Activation (never auto-activate) */ import { Hono } from 'hono'; import { z } from 'zod'; import type { Env } from '../types/env'; import type { WorkflowJob, DeploymentResult } from '../types/workflow'; import { createAuditEvent } from '../utils/audit'; export const deployWorkflowRoute = new Hono<{ Bindings: Env }>(); const DeployRequestSchema = z.object({ jobId: z.string().min(1), }); deployWorkflowRoute.post('/deploy', async (c) => { const body = await c.req.json().catch(() => null); if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400); const parsed = DeployRequestSchema.safeParse(body); if (!parsed.success) { return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400); } const { jobId } = parsed.data; const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text'); if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404); const job = JSON.parse(raw) as WorkflowJob; // ─── Pipeline Gates ────────────────────────────────────────────────────── if (!job.compiledWorkflow) { return c.json({ success: false, error: 'Workflow not compiled. Run /generate first.' }, 409); } if (!job.validationReport?.valid) { return c.json({ success: false, error: 'Workflow failed validation. Fix issues before deploying.', validationReport: job.validationReport, }, 409); } if (!job.simulationReport?.passed) { return c.json({ success: false, error: 'Workflow failed dry-run simulation. Fix issues before deploying.', simulationReport: job.simulationReport, }, 409); } // ─── Credential check via n8n API ──────────────────────────────────────── const credResp = await fetch(`${c.env.N8N_BASE_URL}/api/v1/credentials`, { headers: { 'X-N8N-API-KEY': c.env.N8N_API_KEY }, signal: AbortSignal.timeout(8000), }).catch(() => null); let availableCredentials: Array<{ id: string; name: string; type: string }> = []; if (credResp?.ok) { const credData = await credResp.json() as { data: typeof availableCredentials }; availableCredentials = credData.data ?? []; } // ─── Deploy to n8n (create or update) ──────────────────────────────────── const workflowPayload = { ...job.compiledWorkflow, active: false, // NEVER activate on deploy }; let n8nResponse: Response; let n8nWorkflowId = job.deploymentResult?.n8nWorkflowId; try { if (n8nWorkflowId) { // Update existing n8nResponse = await fetch(`${c.env.N8N_BASE_URL}/api/v1/workflows/${n8nWorkflowId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': c.env.N8N_API_KEY, }, body: JSON.stringify(workflowPayload), signal: AbortSignal.timeout(10000), }); } else { // Create new n8nResponse = await fetch(`${c.env.N8N_BASE_URL}/api/v1/workflows`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': c.env.N8N_API_KEY, }, body: JSON.stringify(workflowPayload), signal: AbortSignal.timeout(10000), }); } } catch (err) { const msg = err instanceof Error ? err.message : 'n8n API unreachable'; return c.json({ success: false, error: `Deployment failed: ${msg}`, jobId }, 502); } if (!n8nResponse.ok) { const errText = await n8nResponse.text(); const deploymentResult: DeploymentResult = { jobId, status: 'failed', deployedAt: new Date().toISOString(), errorMessage: `n8n API ${n8nResponse.status}: ${errText}`, }; await updateJobInKV(c.env.WFO_CACHE, jobId, job, { state: 'simulated', // revert to pre-deploy state deploymentResult, event: 'deployment.failed', }); return c.json({ success: false, error: deploymentResult.errorMessage, deploymentResult, jobId }, 500); } const n8nData = await n8nResponse.json() as { id: string }; n8nWorkflowId = n8nData.id; const deploymentResult: DeploymentResult = { jobId, n8nWorkflowId, status: 'success', deployedAt: new Date().toISOString(), }; await updateJobInKV(c.env.WFO_CACHE, jobId, job, { state: 'deployed', deploymentResult, event: 'deployment.success', credentialAnalysis: { jobId, allCredentialsPresent: availableCredentials.length > 0, required: [], available: availableCredentials, missing: [], analysedAt: new Date().toISOString(), }, }); return c.json({ success: true, jobId, n8nWorkflowId, deploymentResult, message: 'Workflow deployed successfully. It is NOT yet activated. Submit /approve to request activation.', }); }); async function updateJobInKV( kv: KVNamespace, jobId: string, job: WorkflowJob, updates: Partial & { event: string }, ) { const { event, ...rest } = updates; const updated: WorkflowJob = { ...job, ...rest, updatedAt: new Date().toISOString(), auditLog: [ ...job.auditLog, createAuditEvent(jobId, event, 'system', rest as Record), ], }; await kv.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 }); }