/** * deployTools.ts — G-05: Deploy Connectors (CF Pages diretta) * * Tool `cf_pages_status`: chiama la CF Pages API DIRETTAMENTE dal browser, * senza passare per il backend HF Space (che può essere freddo/down). * * Usa VITE_CF_API_TOKEN + VITE_CF_ACCOUNT_ID iniettati al build time * (già presenti in vite.config.ts come injection targets). * * Completa `check_deploy` (che richiede backend up) con un fallback robusto. * * @module deployTools */ import { makeTimedSignal } from "./networkConstants"; // GAP-C: import ErrorSignatureBuilder for deterministic error parsing in loop import { buildErrorSignature } from "./errorSignatureBuilder"; const CF_BASE = "https://api.cloudflare.com/client/v4"; function cfHeaders(): { Authorization: string; "Content-Type": string } | null { const token = (import.meta.env.VITE_CF_API_TOKEN ?? "") as string; const accountId = (import.meta.env.VITE_CF_ACCOUNT_ID ?? "") as string; if (!token || !accountId) return null; return { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }; } function cfAccountId(): string { return (import.meta.env.VITE_CF_ACCOUNT_ID ?? "") as string; } /** Emoji e label per lo stato CF Pages */ function stageIcon(status: string): string { if (status === "success") return "✅"; if (status === "failure" || status === "failed") return "❌"; if (status === "active") return "🔄"; if (status === "skipped") return "⏭️"; return "⏳"; } // ─── cf_pages_status ───────────────────────────────────────────────────────── export async function handleCfPagesStatus(args: { project_name?: string; show_logs?: boolean; }): Promise { const headers = cfHeaders(); if (!headers) { return [ "❌ **cf_pages_status**: credenziali Cloudflare non configurate.", "Aggiungi `CF_API_TOKEN` e `CF_ACCOUNT_ID` nei secrets del progetto CF Pages.", "I valori vengono iniettati da `vite.config.ts` al momento del build.", ].join("\n"); } const accountId = cfAccountId(); const projectName = (args.project_name ?? "agente-ai").trim(); const BASE = `${CF_BASE}/accounts/${accountId}/pages/projects`; const sig = makeTimedSignal(12_000); try { // ── 1. Ottieni ultimi 5 deployment ─────────────────────────────────────── const deployRes = await fetch( `${BASE}/${projectName}/deployments?per_page=5`, { headers, signal: sig }, ); if (!deployRes.ok) { if (deployRes.status === 404) { // Prova a listare i progetti disponibili const projRes = await fetch(BASE, { headers, signal: makeTimedSignal(8_000) }); if (projRes.ok) { const proj = await projRes.json() as { result?: { name: string }[] }; const names = (proj.result ?? []).map(p => p.name).join(", "); return `❌ Progetto "${projectName}" non trovato su CF Pages.\n📋 Progetti disponibili: ${names || "(nessuno)"}`; } } const body = await deployRes.text().catch(() => ""); return `❌ CF Pages API HTTP ${deployRes.status}: ${body.slice(0, 200)}`; } const data = await deployRes.json() as { result?: Array<{ id: string; created_on: string; url?: string; aliases?: string[]; stages?: Array<{ name: string; status: string; started_on?: string; ended_on?: string }>; latest_stage?: { name: string; status: string }; build_config?: { build_command?: string }; deployment_trigger?: { type: string }; }>; }; const deployments = data.result ?? []; if (deployments.length === 0) { return `ℹ️ Nessun deployment trovato per "${projectName}".`; } const latest = deployments[0]; const status = latest.latest_stage?.status ?? latest.stages?.at(-1)?.status ?? "unknown"; const liveUrl = latest.aliases?.[0] ?? latest.url ?? ""; const createdAt = new Date(latest.created_on).toLocaleString("it-IT", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }); const trigger = latest.deployment_trigger?.type === "github" ? "push GitHub" : latest.deployment_trigger?.type === "api" ? "API trigger" : "manuale"; const lines: string[] = [ `${stageIcon(status)} **CF Pages — \`${projectName}\`**`, `📅 ${createdAt} · trigger: ${trigger}`, `🏷️ Stato: \`${status}\``, liveUrl ? `🌐 ${liveUrl}` : "", "", ]; // Stadi build con durata const stages = latest.stages ?? []; if (stages.length > 0) { lines.push("**Stadi:**"); for (const s of stages) { const dur = s.started_on && s.ended_on ? ` (${Math.round((+new Date(s.ended_on) - +new Date(s.started_on)) / 1000)}s)` : ""; lines.push(` ${stageIcon(s.status)} \`${s.name}\`${dur}`); } lines.push(""); } // Cronologia recente if (deployments.length > 1) { lines.push("**Ultimi deployment:**"); for (const d of deployments.slice(0, 4)) { const st = d.latest_stage?.status ?? "?"; const dt = new Date(d.created_on).toLocaleString("it-IT", { day:"2-digit", month:"short", hour:"2-digit", minute:"2-digit" }); lines.push(` ${stageIcon(st)} ${dt} — \`${st}\` [${d.id.slice(0, 8)}]`); } } return lines.filter((l, i) => l !== "" || i !== lines.length - 1).join("\n"); } catch (e) { if (e instanceof Error && e.name === "TimeoutError") { return "⏱️ cf_pages_status: timeout (CF API >12s). Riprova tra qualche secondo."; } return `❌ cf_pages_status: ${String(e).slice(0, 250)}`; } } // ─── deploy_and_verify ───────────────────────────────────────────────────── /** * Fix B: Polls CF Pages build status until success/failure or timeout. * Chiamato DOPO un git_push che trigga un deployment CF Pages. * NON avvia un nuovo deploy — monitora quello già in corso. * * @param args.project_name CF Pages project (default: "agente-ai") * @param args.timeout_s Max attesa in secondi (default: 90, max: 180) */ export async function handleDeployAndVerify(args: { project_name?: string; timeout_s?: number; }): Promise { const headers = cfHeaders(); if (!headers) { return [ "❌ **deploy_and_verify**: credenziali CF non configurate.", "Aggiungi `CF_API_TOKEN` e `CF_ACCOUNT_ID` nei secrets.", ].join("\n"); } const projectName = (args.project_name ?? "agente-ai").trim(); const maxMs = Math.min((args.timeout_s ?? 90), 180) * 1_000; const pollMs = 6_000; const start = Date.now(); let attempt = 0; // eslint-disable-next-line no-constant-condition while (true) { attempt++; const snap = await handleCfPagesStatus({ project_name: projectName }); const elapsed = Math.round((Date.now() - start) / 1_000); const firstLine = snap.split("\n")[0] ?? ""; if (firstLine.includes("✅")) { return `✅ **deploy_and_verify**: build completata in ${elapsed}s (${attempt} poll).\n\n${snap}`; } if (firstLine.includes("❌")) { return `❌ **deploy_and_verify**: build fallita dopo ${elapsed}s.\n\n${snap}`; } if (Date.now() - start + pollMs > maxMs) { return `⏱️ **deploy_and_verify**: timeout ${args.timeout_s ?? 90}s — build ancora in corso.\n\nUltimo stato:\n${snap.slice(0, 500)}`; } await new Promise(r => setTimeout(r, pollMs)); } } // ─── auto_deploy_loop ────────────────────────────────────────────────────────── /** * GAP-C v2: Deploy loop corretto — single-shot, stateless. * * Una chiamata = un tentativo. Dopo un fallimento ritorna gli errori strutturati * + istruzioni esplicite all'agente LLM per applicare il fix e richiamare. * * Flusso corretto: * Chiamata 1 (attempt=1, monitor_only=false): * trigger deploy → poll → se fallisce → ritorna errori → STOP * Agente: legge errori → applica fix → git_push (trigga CF Pages da solo) * Chiamata 2 (attempt=2, monitor_only=true): * monitora nuovo deploy → se fallisce → ritorna errori → STOP * ...fino a attempt === max_attempts * * Bug rimosso: il for loop interno ritriggerava il deploy senza che l'agente * avesse applicato nessun fix — ogni tentativo riceveva lo stesso codice rotto. * * @param args.attempt Tentativo corrente 1-based (default 1). L'agente incrementa. * @param args.monitor_only true sui retry: il git_push ha già triggerato CF Pages. */ export async function handleAutoDeployLoop(args: { project_name?: string; timeout_s?: number; max_attempts?: number; monitor_only?: boolean; /** Tentativo corrente 1-based. L'agente lo incrementa ad ogni retry dopo un fix. */ attempt?: number; }): Promise { const headers = cfHeaders(); if (!headers) { return [ "❌ **auto_deploy_loop**: credenziali CF non configurate.", "Aggiungi VITE_CF_API_TOKEN e VITE_CF_ACCOUNT_ID nei secrets CF Pages.", ].join("\n"); } const projectName = (args.project_name ?? "agente-ai").trim(); const maxAttempts = Math.min(args.max_attempts ?? 3, 5); const attempt = Math.max(args.attempt ?? 1, 1); const accountId = cfAccountId(); const BASE = `${CF_BASE}/accounts/${accountId}/pages/projects`; const lines: string[] = [ `🔁 **auto_deploy_loop** — \`${projectName}\` — tentativo ${attempt}/${maxAttempts}`, ]; // ── Step 1: Trigger (solo se !monitor_only) ────────────────────────────── // Sui retry NON si triggera: il git_push ha già avviato un nuovo CF deployment. if (!args.monitor_only) { try { const triggerRes = await fetch( `${BASE}/${projectName}/deployments`, { method: "POST", headers, signal: makeTimedSignal(10_000) }, ); if (triggerRes.ok) { const tData = await triggerRes.json() as { result?: { id?: string } }; lines.push(`⚡ Deploy triggerato: ID ${tData.result?.id ?? "?"}`); } else if (triggerRes.status === 400) { lines.push(`ℹ️ Nessun nuovo commit — monitoro deployment corrente`); } else { lines.push(`⚠️ Trigger HTTP ${triggerRes.status} — monitoro deployment corrente`); } } catch { lines.push(`⚠️ Trigger fallito — monitoro deployment corrente`); } } else { lines.push(`👁️ monitor_only — monitoro deployment avviato dal git_push`); } // ── Step 2: Poll fino a success/failure ───────────────────────────────── const pollResult = await handleDeployAndVerify({ project_name: projectName, timeout_s: args.timeout_s ?? 90, }); lines.push(pollResult); // ── Step 3a: Successo ──────────────────────────────────────────────────── if (pollResult.startsWith("✅")) { lines.push(`\n✅ **auto_deploy_loop**: deploy completato al tentativo ${attempt}/${maxAttempts}.`); return lines.join("\n"); } // ── Step 3b: Timeout — build ancora in corso ───────────────────────────── if (pollResult.startsWith("⏱️")) { lines.push([ `\n⏱️ **auto_deploy_loop**: build ancora in corso dopo ${args.timeout_s ?? 90}s.`, `📋 Richiama con \`monitor_only: true, attempt: ${attempt}, max_attempts: ${maxAttempts}\` per continuare il monitoring.`, ].join("\n")); return lines.join("\n"); } // ── Step 3c: Build fallita — estrai errori e RITORNA SENZA RETRY ───────── // Il loop NON riesegue qui. L'agente deve: // 1. Leggere gli errori sotto // 2. Applicare i fix (git_push con le correzioni) // 3. Richiamare auto_deploy_loop con monitor_only:true, attempt:N+1 let buildErrors = ""; let errorSig = ""; try { const depRes = await fetch( `${BASE}/${projectName}/deployments?per_page=1`, { headers, signal: makeTimedSignal(8_000) }, ); if (depRes.ok) { const depData = await depRes.json() as { result?: Array<{ id: string; stages?: Array<{ name: string; status: string }> }> }; const latestId = depData.result?.[0]?.id; if (latestId) { const logRes = await fetch( `${BASE}/${projectName}/deployments/${latestId}/history/logs?size=100`, { headers, signal: makeTimedSignal(8_000) }, ); if (logRes.ok) { const logData = await logRes.json() as { result?: { logs?: Array<{ message: string; level?: string }> } }; const allLogs = (logData.result?.logs ?? []).map(l => l.message).join("\n"); const errorLines = allLogs .split("\n") .filter(l => l.includes("error TS") || l.includes("ERROR") || l.includes("Build failed") || l.includes("Cannot find") || l.includes("Type error") || (/\(\d+,\d+\)/.test(l) && l.includes("error")), ) .slice(0, 20) .join("\n"); if (errorLines.trim()) { buildErrors = errorLines; errorSig = JSON.stringify(buildErrorSignature(errorLines, "deploy_monitor", {}, 0, 0)); } } } } } catch { /* non-fatal — continuiamo senza errori estratti */ } if (buildErrors) { lines.push(`\n🔍 **Build errors (tentativo ${attempt})**:\n\`\`\`\n${buildErrors}\n\`\`\``); if (errorSig) lines.push(`🔑 Error signature: \`${errorSig}\``); } else { lines.push(`\n🔍 Nessun errore TypeScript estratto — controlla la pipeline CF Pages manualmente.`); } // ── Step 4: Istruzioni precise per l'agente LLM ────────────────────────── if (attempt < maxAttempts) { lines.push([ `\n⚠️ **auto_deploy_loop**: build fallita — tentativo ${attempt}/${maxAttempts}.`, ``, `📋 **Prossimi passi (in ordine):**`, ` 1. Analizza e correggi gli errori sopra nei file indicati`, ` 2. Usa \`git_push\` per pushare le correzioni (trigga CF Pages automaticamente)`, ` 3. Richiama \`auto_deploy_loop\` con:`, ` \`monitor_only: true\` ← non ritriggerare, il push ha già avviato il deploy`, ` \`attempt: ${attempt + 1}\``, ` \`max_attempts: ${maxAttempts}\``, ].join("\n")); } else { lines.push([ `\n❌ **auto_deploy_loop**: max tentativi raggiunti (${attempt}/${maxAttempts}).`, `Intervento manuale necessario — controlla la pipeline CF Pages.`, ].join("\n")); } return lines.join("\n"); }