feat: initialize worker for website generator with GitHub deployment functionality
- 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.
This commit is contained in:
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# Redis Configuration
|
||||||
|
# ------------------------------
|
||||||
|
REDIS_URL=redis://username:your_password@HOST:PORT
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# GitHub Configuration
|
||||||
|
# ------------------------------
|
||||||
|
# Scopes required: 'repo' (to create repos and push code)
|
||||||
|
GITHUB_TOKEN=ghp_xxxxxx
|
||||||
|
|
||||||
|
# The GitHub username associated with the token above
|
||||||
|
GITHUB_USERNAME=github_username
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
trigger.sh
|
||||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build TypeScript
|
||||||
|
RUN npx tsc
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install only production dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy built files from builder
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Set environment
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Run the worker
|
||||||
|
CMD ["node", "dist/worker.js"]
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Worker Setup for Website Generator
|
||||||
|
|
||||||
|
|
||||||
|
## Docker Installation
|
||||||
|
|
||||||
|
This project requires Docker to be installed on your system.
|
||||||
|
1. **Install Docker**: Follow the official installation guide for your operating system:
|
||||||
|
* [Install Docker Engine](https://docs.docker.com/engine/install/)
|
||||||
|
|
||||||
|
2. **Post-installation steps for Linux**: If you are on Linux, follow the post-installation steps to manage Docker as a non-root user.
|
||||||
|
* [Linux post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/)
|
||||||
|
|
||||||
|
3. **Verify Installation**: Run the following command to verify that Docker is installed correctly.
|
||||||
|
```bash
|
||||||
|
docker run hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
# Deployment -
|
||||||
|
|
||||||
|
## Setup environment variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ------------------------------
|
||||||
|
# Redis Configuration
|
||||||
|
# ------------------------------
|
||||||
|
REDIS_URL=redis://username:your_password@HOST:PORT
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# GitHub Configuration
|
||||||
|
# ------------------------------
|
||||||
|
# Scopes required: 'repo' (to create repos and push code)
|
||||||
|
GITHUB_TOKEN=ghp_xxxxxx
|
||||||
|
|
||||||
|
# The GitHub username associated with the token above
|
||||||
|
GITHUB_USERNAME=github_username
|
||||||
|
```
|
||||||
|
|
||||||
|
# Build and run
|
||||||
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
docker-compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f worker
|
||||||
|
```
|
||||||
|
# Restart worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose restart worker
|
||||||
|
```
|
||||||
|
# Stop worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
# Rebuild after code changes
|
||||||
|
```bash
|
||||||
|
docker-compose up --build -d
|
||||||
|
```
|
||||||
23
docker-compose.yaml
Normal file
23
docker-compose.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: production
|
||||||
|
container_name: website-generator-worker
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- REDIS_URL=${REDIS_URL}
|
||||||
|
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||||
|
- GITHUB_USERNAME=${GITHUB_USERNAME}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
1151
package-lock.json
generated
Normal file
1151
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "worker-for-website-generator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/worker.js",
|
||||||
|
"dev": "nodemon --exec ts-node worker.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/rest": "^21.0.0",
|
||||||
|
"bullmq": "^5.66.4",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"ioredis": "^5.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"nodemon": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
redis.ts
Normal file
11
redis.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ConnectionOptions } from "bullmq";
|
||||||
|
|
||||||
|
const { REDIS_URL } = process.env;
|
||||||
|
|
||||||
|
if (!REDIS_URL) {
|
||||||
|
throw new Error("Missing REDIS_URL environment variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redisConfig: ConnectionOptions = {
|
||||||
|
url: REDIS_URL,
|
||||||
|
};
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
159
worker.ts
Normal file
159
worker.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user