- Added package.json for project dependencies and scripts. - Created redis.ts for Redis connection configuration. - Set up tsconfig.json for TypeScript compilation settings. - Implemented worker.ts to handle GitHub repository deployments using BullMQ and Octokit.
160 lines
4.4 KiB
TypeScript
160 lines
4.4 KiB
TypeScript
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<string, string>;
|
|
}
|
|
|
|
const worker = new Worker<DeployJobData>(
|
|
"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}`);
|
|
});
|