import { Job, Worker } from "bullmq"; import dotenv from "dotenv"; import { redisConfig } from "./redis.js"; dotenv.config(); console.log("👷 Gitea Deploy Worker Started..."); export interface DeployJobData { repoName: string; chatId: string; files: Record; } // Configuration const GITEA_API_URL = process.env.GITEA_API_URL || "https://git.mworld.cloud/api/v1"; const GITEA_TOKEN = process.env.GITEA_TOKEN; const GITEA_USERNAME = process.env.GITEA_USERNAME; if (!GITEA_TOKEN || !GITEA_USERNAME) { throw new Error("Missing GITEA_TOKEN or GITEA_USERNAME"); } // Helper to call Gitea API async function callGitea(method: string, path: string, body?: any) { const headers: Record = { Authorization: `token ${GITEA_TOKEN}`, "Content-Type": "application/json", }; const response = await fetch(`${GITEA_API_URL}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok && response.status !== 404) { const errorText = await response.text(); throw new Error(`Gitea API Error ${response.status}: ${errorText}`); } return response; } const worker = new Worker( "github-deployments", async (job: Job) => { const { repoName, chatId, files } = job.data; // 1. Sanitize Repo Name const sanitizedRepoName = repoName .toLowerCase() .trim() .replace(/\s+/g, "-") .replace(/[^\w-]/g, ""); console.log( `[Job ${job.id}] Processing deployment for ${sanitizedRepoName} (Chat: ${chatId})...` ); try { await job.updateProgress(10); let repoDetails: any; const checkRepo = await callGitea("GET", `/repos/${GITEA_USERNAME}/${sanitizedRepoName}`); if (checkRepo.status === 404) { console.log(`[Job ${job.id}] Repo not found, creating new repo: ${sanitizedRepoName}`); // Create Repo const createRes = await callGitea("POST", "/user/repos", { name: sanitizedRepoName, private: true, auto_init: true, description: `Generated by MWorld for chat ${chatId}`, }); if (!createRes.ok) throw new Error("Failed to create repository"); repoDetails = await createRes.json(); await new Promise((r) => setTimeout(r, 2000)); } else { repoDetails = await checkRepo.json(); } const defaultBranch = repoDetails.default_branch || "main"; // await job.updateProgress(30); // 3. Prepare Batch File Operations const fileOperations = Object.entries(files).map(([path, content]) => ({ operation: "upload", path: path, content: Buffer.from(content as string).toString("base64"), })); await job.updateProgress(60); // 4. Commit and Push (Atomic Batch Operation) const commitRes = await callGitea( "POST", `/repos/${GITEA_USERNAME}/${sanitizedRepoName}/contents`, { branch: defaultBranch, message: `Deploy updates from MWorld ⚡️ (Chat ${chatId})`, files: fileOperations, author: { name: "MWorld Bot", email: "bot@mworld.com", }, } ); if (!commitRes.ok) { throw new Error(`Failed to commit files: ${await commitRes.text()}`); } await job.updateProgress(100); const repoUrl = `${GITEA_API_URL.replace("/api/v1", "")}/${GITEA_USERNAME}/${sanitizedRepoName}`; console.log(`[Job ${job.id}] Deployment Complete! 🚀 URL: ${repoUrl}`); return { success: true, url: repoUrl }; } catch (error: any) { console.error(`[Job ${job.id}] Failed:`, error.message); throw error; } }, { connection: redisConfig, concurrency: 5, limiter: { max: 10, duration: 1000, }, } ); // Listener for errors worker.on("failed", (job, err) => { console.error(`[Job ${job?.id}] Failed with error ${err.message}`); });