PYAE1994's picture
Upload folder using huggingface_hub
dd480ef verified
/**
* 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<WorkflowJob> & { 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<string, unknown>),
],
};
await kv.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
}