import { Hono } from "hono"; import { logger } from "hono/logger"; import { serve } from "@hono/node-server"; import cron from "node-cron"; import { loadConfig } from "./config.js"; import { CoderClient } from "./services/coder.js"; import { GiteaClient } from "./services/gitea.js"; import { TaskQueue } from "./queue.js"; import { issueTriage } from "./handlers/issue-triage.js"; import { issueLabel } from "./handlers/issue-label.js"; import { issueComment } from "./handlers/issue-comment.js"; import { prReview } from "./handlers/pr-review.js"; import { prRework } from "./handlers/pr-rework.js"; import { release } from "./handlers/release.js"; import { runMaintenance, type MaintenanceRepo } from "./handlers/maintenance.js"; // --------------------------------------------------------------------------- // Bootstrap // --------------------------------------------------------------------------- const config = loadConfig(); const coderClient = new CoderClient(config); const giteaClient = new GiteaClient(config); const concurrency = parseInt(process.env.QUEUE_CONCURRENCY || "2", 10); const queue = new TaskQueue(coderClient, giteaClient, concurrency); const app = new Hono(); app.use("*", logger()); // --------------------------------------------------------------------------- // Health check + queue status // --------------------------------------------------------------------------- app.get("/health", (c) => c.json({ status: "ok" })); app.get("/queue", (c) => c.json(queue.status())); // --------------------------------------------------------------------------- // Webhook endpoints — same paths as the n8n webhooks so Gitea config // can stay unchanged (just point to new host). // --------------------------------------------------------------------------- app.post("/webhook/gitea-issue-triage", issueTriage(config, queue)); app.post("/webhook/gitea-issue-label", issueLabel(config, queue)); app.post("/webhook/gitea-issue-comment", issueComment(config, queue)); app.post("/webhook/gitea-pr-review", prReview(config, queue, giteaClient)); app.post("/webhook/gitea-pr-review-rework", prRework(config, queue)); app.post("/webhook/gitea-release", release(config, queue)); // --------------------------------------------------------------------------- // Task-complete callback — workspaces call this when done // --------------------------------------------------------------------------- app.post("/webhook/task-complete/:name", async (c) => { const name = c.req.param("name"); console.log(`[callback] task-complete received for: ${name}`); const found = await queue.taskComplete(name); return c.json({ ok: true, found }); }); // --------------------------------------------------------------------------- // Scheduled maintenance cron (Monday 9:00 AM) // --------------------------------------------------------------------------- const maintenanceRepos = parseMaintenanceRepos(); if (maintenanceRepos.length > 0) { const maintenanceFn = runMaintenance(config, queue, maintenanceRepos); cron.schedule("0 9 * * 1", () => { console.log("[cron] running weekly maintenance"); maintenanceFn().catch((err) => console.error("[cron] maintenance failed:", err), ); }); console.log( `Maintenance cron scheduled for ${maintenanceRepos.length} repo(s)`, ); } // --------------------------------------------------------------------------- // Start // --------------------------------------------------------------------------- // Reconcile queue state with Coder before accepting traffic queue.reconcile().catch((err) => console.error("[startup] reconcile failed:", err), ); serve({ fetch: app.fetch, port: config.port }, (info) => { console.log(`SDLC Orchestrator listening on :${info.port} (concurrency: ${concurrency})`); }); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Parse MAINTENANCE_REPOS env var. * Format: comma-separated "org/repo=clone_url" entries. * Example: "claude/babble=https://gitea.samson.media/claude/babble.git" */ function parseMaintenanceRepos(): MaintenanceRepo[] { const raw = process.env.MAINTENANCE_REPOS; if (!raw) return []; return raw.split(",").map((entry) => { const [fullName, cloneUrl] = entry.trim().split("="); const [org, repo] = fullName.split("/"); return { org, repo, cloneUrl }; }); }