AUDIT / src /lib /agentLoop /deployTools.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
15.3 kB
/**
* 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<string> {
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<string> {
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<void>(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<string> {
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");
}