Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-22 14:35:37 +00:00
parent 7de4fe7054
commit 2b45297603
4 changed files with 524 additions and 0 deletions

View file

@ -0,0 +1,116 @@
// railway-webhook (público)
// Recebe eventos do Railway (Project Settings → Webhooks) sobre deploys.
// Quando um deployment SUCCESS bate em um railway_service_id que conhecemos,
// marcamos o agent_instance como 'active'.
//
// Payload Railway (resumido):
// { type: "DEPLOY", deployment: { id, status, serviceId, environmentId, ... }, project, ... }
// Status possíveis: BUILDING, DEPLOYING, SUCCESS, FAILED, CRASHED, REMOVED
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { corsHeaders } from "../_shared/cors.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
interface RailwayWebhookPayload {
type?: string;
deployment?: {
id?: string;
status?: string;
serviceId?: string;
environmentId?: string;
};
// Railway envia variantes; aceitamos serviceId/serviço em vários lugares
service?: { id?: string };
status?: string;
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
if (req.method !== "POST") {
return jsonResponse(405, { error: "method not allowed" });
}
let payload: RailwayWebhookPayload;
try {
payload = await req.json();
} catch {
return jsonResponse(400, { error: "invalid json" });
}
const serviceId =
payload.deployment?.serviceId ?? payload.service?.id ?? null;
const status = payload.deployment?.status ?? payload.status ?? null;
if (!serviceId || !status) {
console.log("railway-webhook: payload sem serviceId/status — ignorando", payload);
return jsonResponse(200, { ignored: true });
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { data: agent } = await supabase
.from("agent_instances")
.select("id, status")
.eq("railway_service_id", serviceId)
.maybeSingle();
if (!agent) {
console.log(`railway-webhook: serviceId ${serviceId} não corresponde a nenhum agent_instance`);
return jsonResponse(200, { ignored: true });
}
const now = new Date().toISOString();
const upper = status.toUpperCase();
if (upper === "SUCCESS") {
await supabase
.from("agent_instances")
.update({
status: "active",
provisioned_at: now,
last_health_check_at: now,
})
.eq("id", agent.id);
await supabase
.from("provisioning_jobs")
.update({ status: "completed", completed_at: now })
.eq("agent_instance_id", agent.id)
.in("status", ["running", "retrying"]);
return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "active" });
}
if (upper === "FAILED" || upper === "CRASHED") {
await supabase
.from("agent_instances")
.update({ status: "error" })
.eq("id", agent.id);
await supabase
.from("provisioning_jobs")
.update({
status: "failed",
error_message: `Railway deployment ${upper}`,
completed_at: now,
})
.eq("agent_instance_id", agent.id)
.in("status", ["running", "retrying"]);
return jsonResponse(200, { ok: true, agent_id: agent.id, new_status: "error" });
}
// BUILDING / DEPLOYING / outros — apenas log
return jsonResponse(200, { ok: true, ignored_status: upper });
});
function jsonResponse(status: number, body: unknown) {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}