Skip to content

Instantly share code, notes, and snippets.

@maietta
Created January 17, 2025 04:46
Show Gist options
  • Save maietta/f49e12d2c42c87ac78d47be3c5a15f84 to your computer and use it in GitHub Desktop.
Save maietta/f49e12d2c42c87ac78d47be3c5a15f84 to your computer and use it in GitHub Desktop.
Gitea Webhook Proxy for Coolify
COOLIFY_API_URL=https://coolify.yourdomain.com
COOLIFY_API_KEY=abc123
WEBHOOKS_SECRET=shared-secret-between-gitea-and-deploy
import { serve } from "bun";
import crypto from "crypto";
// Extract repository name and branch from payload
function extractRepoInfo(payload: { repository: { full_name: string }; ref: string }) {
const repoName = payload?.repository?.full_name || "";
const branch = payload?.ref?.split("/").pop() || "";
return { repoName, branch };
}
// Normalize repo URL to ensure proper comparison
function normalizeRepoUrl(url: string) {
// Remove protocol (http/https) and .git, then convert to lowercase
const normalizedUrl = url
.replace(/^https?:\/\/|\.git$/g, "") // Remove "http(s)://" and ".git" suffix
.toLowerCase();
// Handle git@ format (git@domain:user/repo.git -> user/repo)
if (normalizedUrl.startsWith("git@")) {
return normalizedUrl.split(":")[1].replace(".git", "").toLowerCase();
}
return normalizedUrl;
}
// Environment variables
const apiUrl = `${process.env.COOLIFY_API_URL}/api/v1`;
const token = `Bearer ${process.env.COOLIFY_API_KEY}`;
const secret = process.env.WEBHOOKS_SECRET!;
// Verify HMAC-SHA256 signature
function verifyHmacSignature(payload: string, receivedSignature: string | null, secretKey: string): boolean {
if (!receivedSignature) return false;
const expectedSignature = crypto.createHmac("sha256", secretKey).update(payload, "utf8").digest("hex");
console.log(`Expected Signature: ${expectedSignature}`);
console.log(`Received Signature: ${receivedSignature}`);
return expectedSignature === receivedSignature.trim().toLowerCase();
}
// Fetch Coolify API list
async function getApiList(): Promise<any[]> {
try {
const response = await fetch(`${apiUrl}/applications`, {
headers: { Authorization: token },
});
if (!response.ok) throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
return await response.json();
} catch (err) {
console.error("Error fetching API list:", err);
throw new Error("Failed to fetch API list.");
}
}
// Trigger a deployment webhook in Coolify
async function triggerWebhook(uuid: string, force: boolean): Promise<any> {
try {
const response = await fetch(`${apiUrl}/deploy?uuid=${uuid}&force=${force}`, {
method: "POST",
headers: {
Authorization: token,
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
return await response.json();
} catch (err) {
console.error("Error triggering webhook:", err);
throw new Error("Failed to trigger deployment.");
}
}
// Handle incoming requests
serve({
port: 3000,
async fetch(req) {
if (req.method === "GET") {
return new Response("OK", { status: 200 });
}
if (req.method === "POST") {
try {
const contentType = req.headers.get("content-type")?.toLowerCase();
if (contentType !== "application/json") {
return new Response(JSON.stringify({ error: "Content-Type must be application/json" }), { status: 400 });
}
const rawPayload = await req.text();
// ✅ Check Gitea signature
const signature = req.headers.get("x-gitea-signature");
if (!signature) {
return new Response(JSON.stringify({ error: "Missing X-Gitea-Signature header" }), { status: 400 });
}
// ✅ Verify HMAC signature
if (!verifyHmacSignature(rawPayload, signature, secret)) {
console.error("Invalid HMAC signature");
return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 403 });
}
const parsedPayload = JSON.parse(rawPayload);
const { repoName, branch } = extractRepoInfo(parsedPayload);
if (!repoName || !branch) {
return new Response(JSON.stringify({ error: "Missing repository name or branch" }), { status: 400 });
}
console.log(`Received Webhook - Repo: ${repoName}, Branch: ${branch}`);
// ✅ Fetch Coolify API list and match repository + branch
const apiList = await getApiList();
const normalizedRepoName = normalizeRepoUrl(repoName);
const matchedRepo = apiList.find(
(item: { git_repository: string; git_branch: string }) =>
normalizeRepoUrl(item.git_repository) === normalizedRepoName &&
item.git_branch.trim().toLowerCase() === branch.toLowerCase()
);
if (!matchedRepo) {
console.log(`No match found for ${repoName} (${branch})`);
return new Response(JSON.stringify({ error: "No matching repository and branch" }), { status: 404 });
}
console.log(`Match Found! Deploying ${repoName} (${branch})`);
await triggerWebhook(matchedRepo.uuid, false);
return new Response(JSON.stringify({ message: "Deployment triggered successfully" }), { status: 200 });
} catch (err) {
console.error("Error processing webhook:", err);
return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500 });
}
}
return new Response(JSON.stringify({ error: "Method Not Allowed" }), { status: 405 });
},
});
console.log("Server running at http://localhost:3000/");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment