import { Octokit } from "@octokit/rest"; import { Job, Worker } from "bullmq"; import dotenv from "dotenv"; import { redisConfig } from "./redis.js"; dotenv.config(); console.log("👷 Deploy Worker Started..."); export interface DeployJobData { repoName: string; chatId: string; files: Record; } const worker = new Worker( "github-deployments", async (job: Job) => { const { repoName, chatId, files, } = job.data; // 1. Sanitize Repo Name (e.g. "Hono Vite Health Check" -> "hono-vite-health-check") const sanitizedRepoName = repoName .toLowerCase() .trim() .replace(/\s+/g, "-") .replace(/[^\w-]/g, ""); console.log( `[Job ${job.id}] Processing deployment for ${sanitizedRepoName} (Chat: ${chatId})...` ); // 2. Auth const token = process.env.GITHUB_TOKEN; const username = process.env.GITHUB_USERNAME; if (!token || !username) { throw new Error("Missing GitHub Token or Username. Cannot deploy."); } // Initialize GitHub Client const octokit = new Octokit({ auth: token }); try { await job.updateProgress(10); // 3. Ensure Repo Exists or Create it let latestCommitSha: string; try { // Check if repo exists await octokit.repos.get({ owner: username, repo: sanitizedRepoName }); // If exists, get HEAD commit const ref = await octokit.git.getRef({ owner: username, repo: sanitizedRepoName, ref: "heads/main", }); latestCommitSha = ref.data.object.sha; } catch (e: any) { if (e.status === 404) { console.log( `[Job ${job.id}] Repo not found, creating new repo: ${sanitizedRepoName}` ); // Create Repo await octokit.repos.createForAuthenticatedUser({ name: sanitizedRepoName, private: true, // Set to true if you want private repos auto_init: true, // Creates an initial commit (README) so we have a HEAD description: `Generated by MWorld for chat ${chatId}`, }); // Wait a brief moment for GitHub propagation await new Promise((r) => setTimeout(r, 2000)); // Get the initial commit SHA const ref = await octokit.git.getRef({ owner: username, repo: sanitizedRepoName, ref: "heads/main", }); latestCommitSha = ref.data.object.sha; } else { throw e; } } await job.updateProgress(30); // 4. Create Tree (Blobs) // The API expects an array of file objects. const treeData = Object.entries(files).map(([path, content]) => ({ path, // e.g., "src/main.jsx" or "package.json" mode: "100644" as const, type: "blob" as const, content: content as string, // The actual string content from Redis })); // Create a tree based on the latest commit const { data: tree } = await octokit.git.createTree({ owner: username, repo: sanitizedRepoName, base_tree: latestCommitSha, tree: treeData, }); await job.updateProgress(60); // 5. Create Commit const { data: commit } = await octokit.git.createCommit({ owner: username, repo: sanitizedRepoName, message: `Deploy updates from MWorld ⚡️ (Chat ${chatId})`, tree: tree.sha, parents: [latestCommitSha], }); await job.updateProgress(80); // 6. Push (Update Reference) await octokit.git.updateRef({ owner: username, repo: sanitizedRepoName, ref: "heads/main", sha: commit.sha, }); await job.updateProgress(100); const repoUrl = `https://github.com/${username}/${sanitizedRepoName}`; console.log(`[Job ${job.id}] Deployment Complete! 🚀 URL: ${repoUrl}`); // TODO: USE SUPABASE TO UPDATE THE REPO URL 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, // Rate limit protection for GitHub API }, } ); // Listener for errors worker.on("failed", (job, err) => { console.error(`[Job ${job?.id}] Failed with error ${err.message}`); });