"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var deploy_exports = {}; __export(deploy_exports, { abortJobForPreview: () => abortJobForPreview, appendLog: () => appendLog, runDeploy: () => runDeploy, signalAbort: () => signalAbort, stopPreview: () => stopPreview, subscribeToLogs: () => subscribeToLogs }); module.exports = __toCommonJS(deploy_exports); var import_db = require("../lib/db"); var import_logger = require("../lib/logger"); var import_encryption = require("../lib/encryption"); var import_adminSettings = require("../lib/adminSettings"); var import_env = require("../lib/env"); var import_ec2 = require("./ec2"); var import_ssh = require("./ssh"); var import_gitea = require("./gitea"); const log = (0, import_logger.createLogger)("DEPLOY"); const activeSshSessions = /* @__PURE__ */ new Map(); function abortJobForPreview(previewId) { const session = activeSshSessions.get(previewId); if (session) { session.abort(); activeSshSessions.delete(previewId); } } async function appendLog(previewId, text) { const settings = await (0, import_adminSettings.getAdminSettings)(); const preview = await import_db.prisma.preview.findUnique({ where: { id: previewId } }); if (!preview) return; let logs = (preview.logs || "") + text; if (Buffer.byteLength(logs, "utf8") > settings.logSizeLimitBytes) { const marker = "--- logs truncated ---\n"; while (Buffer.byteLength(logs, "utf8") > settings.logSizeLimitBytes) { const nl = logs.indexOf("\n"); if (nl === -1) break; logs = logs.slice(nl + 1); } logs = marker + logs; } await import_db.prisma.preview.update({ where: { id: previewId }, data: { logs } }); broadcastLogUpdate(previewId, text); } const logSubscribers = /* @__PURE__ */ new Map(); function subscribeToLogs(previewId, cb) { if (!logSubscribers.has(previewId)) logSubscribers.set(previewId, /* @__PURE__ */ new Set()); logSubscribers.get(previewId).add(cb); return () => logSubscribers.get(previewId)?.delete(cb); } function broadcastLogUpdate(previewId, text) { logSubscribers.get(previewId)?.forEach((cb) => cb(text)); } async function updateStatus(previewId, status, extra = {}) { await import_db.prisma.preview.update({ where: { id: previewId }, data: { status, ...extra } }); } async function updateGiteaComment(user, preview, repoConfig, statusLine, lastLogLines) { const body = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber: preview.prNumber, status: statusLine, commitSha: preview.commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL, lastLogLines, instanceIp: preview.instanceIp ?? void 0, port: preview.port }); if (preview.giteaCommentId) { try { await (0, import_gitea.updateComment)(user, repoConfig.repoOwner, repoConfig.repoName, preview.giteaCommentId, body); } catch (e) { log.warn({ e }, "Failed to update Gitea comment"); } } } async function runDeploy(jobId) { const job = await import_db.prisma.job.findUnique({ where: { id: jobId }, include: { preview: { include: { repoConfig: { include: { user: true } } } } } }); if (!job || !job.preview) { log.error({ jobId }, "Job or preview not found"); return; } await import_db.prisma.job.update({ where: { id: jobId }, data: { status: "RUNNING", startedAt: /* @__PURE__ */ new Date() } }); const preview = job.preview; const repoConfig = preview.repoConfig; const user = preview.repoConfig.user; const payload = job.payload; const { commitSha, prNumber, prTitle, cloneUrl, isFirstDeploy } = payload; try { if (isFirstDeploy) { await firstDeploy(jobId, preview, repoConfig, user, commitSha, prNumber, prTitle, cloneUrl); } else { await redeploy(jobId, preview, repoConfig, user, commitSha, prNumber, prTitle); } await import_db.prisma.job.update({ where: { id: jobId }, data: { status: "DONE", finishedAt: /* @__PURE__ */ new Date() } }); } catch (e) { if (e.message === "ABORTED") { log.info({ jobId, previewId: preview.id }, "Job aborted"); await import_db.prisma.job.update({ where: { id: jobId }, data: { status: "FAILED", error: "Aborted by newer deploy", finishedAt: /* @__PURE__ */ new Date() } }); return; } log.error({ e, jobId, previewId: preview.id }, "Deploy failed"); await import_db.prisma.job.update({ where: { id: jobId }, data: { status: "FAILED", error: e.message, finishedAt: /* @__PURE__ */ new Date() } }); const freshPreview = await import_db.prisma.preview.findUnique({ where: { id: preview.id } }); if (!freshPreview) return; const lastLines = (freshPreview.logs || "").split("\n").slice(-10).join("\n"); await updateStatus(preview.id, "FAILED"); const failBody = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber: freshPreview.prNumber, status: `\u{1F534} Failed \u2014 last log lines: \`\`\` ${lastLines} \`\`\``, commitSha: freshPreview.commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }); if (freshPreview.giteaCommentId) { try { await (0, import_gitea.updateComment)(user, repoConfig.repoOwner, repoConfig.repoName, freshPreview.giteaCommentId, failBody); } catch { } } } } async function firstDeploy(jobId, preview, repoConfig, user, commitSha, prNumber, prTitle, cloneUrl) { const settings = await (0, import_adminSettings.getAdminSettings)(); const ec2 = (0, import_ec2.makeEc2Client)(user); const previewId = preview.id; const activeCount = await import_db.prisma.preview.count({ where: { repoConfig: { userId: user.id }, status: { in: ["PROVISIONING", "BUILDING", "RUNNING"] }, id: { not: previewId } } }); if (activeCount >= settings.maxConcurrentInstancesPerUser) { const body = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: `\u{1F534} Cannot provision preview \u2014 concurrent instance limit (${settings.maxConcurrentInstancesPerUser}) reached.`, commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }); const commentId2 = await (0, import_gitea.postComment)(user, repoConfig.repoOwner, repoConfig.repoName, prNumber, body); await import_db.prisma.preview.update({ where: { id: previewId }, data: { giteaCommentId: commentId2 } }); throw new Error("Concurrent instance limit reached"); } await updateStatus(previewId, "PROVISIONING", { commitSha, prNumber, prTitle }); const commentBody = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: "\u{1F7E1} Provisioning EC2 instance...", commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }); let commentId = await (0, import_gitea.postComment)(user, repoConfig.repoOwner, repoConfig.repoName, prNumber, commentBody); await import_db.prisma.preview.update({ where: { id: previewId }, data: { giteaCommentId: commentId } }); checkAbort(previewId); const keyName = `pp-preview-${previewId}`; const { privateKey } = await (0, import_ec2.generateAndImportKeyPair)(ec2, keyName); const encPrivateKey = (0, import_encryption.encrypt)(privateKey); await import_db.prisma.preview.update({ where: { id: previewId }, data: { sshPrivateKey: encPrivateKey, sshKeyName: keyName } }); checkAbort(previewId); const sgName = `pp-preview-${previewId}`; const securityGroupId = await (0, import_ec2.createPreviewSecurityGroup)(ec2, sgName, repoConfig.port); checkAbort(previewId); const instanceId = await (0, import_ec2.launchInstance)({ ec2, region: user.awsRegion, instanceType: repoConfig.instanceType, keyName, securityGroupId, tags: { "pp:managed": "true", "pp:userId": String(user.id), "pp:repo": `${repoConfig.repoOwner}/${repoConfig.repoName}`, "pp:prNumber": String(prNumber), "pp:previewId": String(previewId) } }); await import_db.prisma.preview.update({ where: { id: previewId }, data: { instanceId } }); await appendLog(previewId, `[PP] EC2 instance ${instanceId} launched. Waiting for it to be running... `); checkAbort(previewId); const instanceIp = await (0, import_ec2.waitForInstanceRunning)(ec2, instanceId); await import_db.prisma.preview.update({ where: { id: previewId }, data: { instanceIp, port: repoConfig.port } }); await (0, import_gitea.updateComment)( user, repoConfig.repoOwner, repoConfig.repoName, commentId, (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: `\u{1F7E1} Building... (EC2 ready at ${instanceIp})`, commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }) ); await appendLog(previewId, `[PP] Instance running at ${instanceIp}. Waiting for SSH... `); checkAbort(previewId); const sshSession = await (0, import_ssh.connectSsh)(instanceIp, privateKey, 3e5); activeSshSessions.set(previewId, sshSession); try { if (repoConfig.aptPackages.length > 0) { await runSshStep(previewId, sshSession, `sudo apt-get install -y ${repoConfig.aptPackages.join(" ")}`); } const giteaPat = user.giteaPAT ? (0, import_encryption.decrypt)(user.giteaPAT) : ""; const authCloneUrl = cloneUrl.replace("https://", `https://${user.giteaUsername}:${giteaPat}@`); await runSshStep(previewId, sshSession, `git clone ${authCloneUrl} /opt/app`); await runSshStep(previewId, sshSession, `cd /opt/app && git fetch origin pull/${prNumber}/head:pp-pr && git checkout pp-pr`); await setupAndBuild(previewId, sshSession, repoConfig, preview, commitSha, true); await updateStatus(previewId, "RUNNING", { commitSha, instanceIp, port: repoConfig.port, lastActivityAt: /* @__PURE__ */ new Date() }); const freshPreview = await import_db.prisma.preview.findUnique({ where: { id: previewId } }); await (0, import_gitea.updateComment)( user, repoConfig.repoOwner, repoConfig.repoName, commentId, (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: `\u{1F7E2} Live at http://${instanceIp}:${repoConfig.port}`, commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }) ); } finally { sshSession.close(); activeSshSessions.delete(previewId); } } async function redeploy(jobId, preview, repoConfig, user, commitSha, prNumber, prTitle) { const previewId = preview.id; const instanceIp = preview.instanceIp; const privateKey = (0, import_encryption.decrypt)(preview.sshPrivateKey); const sshSession = await (0, import_ssh.connectSsh)(instanceIp, privateKey, 3e4); activeSshSessions.set(previewId, sshSession); await appendLog(previewId, ` --- Redeploy: ${commitSha} --- `); const freshPreview = await import_db.prisma.preview.findUnique({ where: { id: previewId } }); const commentBody = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: `\u{1F7E1} Building... (EC2 at ${instanceIp})`, commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }); const newCommentId = await (0, import_gitea.postComment)(user, repoConfig.repoOwner, repoConfig.repoName, prNumber, commentBody); await import_db.prisma.preview.update({ where: { id: previewId }, data: { giteaCommentId: newCommentId, commitSha } }); try { if (repoConfig.useDockerCompose) { const composePath = repoConfig.composeFilePath || "docker-compose.yml"; await runSshStep(previewId, sshSession, `cd /opt/app && docker compose -f ${composePath} down 2>&1 || true`); } else if (freshPreview?.pid) { await runSshStep(previewId, sshSession, `kill ${freshPreview.pid} 2>/dev/null || true; sleep 5; kill -9 ${freshPreview.pid} 2>/dev/null || true`); } await runSshStep(previewId, sshSession, `cd /opt/app && git fetch origin pull/${prNumber}/head:pp-pr && git checkout pp-pr && git reset --hard FETCH_HEAD`); await updateStatus(previewId, "BUILDING"); await setupAndBuild(previewId, sshSession, repoConfig, preview, commitSha, false); await updateStatus(previewId, "RUNNING", { commitSha, lastActivityAt: /* @__PURE__ */ new Date() }); await (0, import_gitea.updateComment)( user, repoConfig.repoOwner, repoConfig.repoName, newCommentId, (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber, status: `\u{1F7E2} Live at http://${instanceIp}:${repoConfig.port}`, commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }) ); } finally { sshSession.close(); activeSshSessions.delete(previewId); } } async function setupAndBuild(previewId, sshSession, repoConfig, preview, commitSha, isFirstProvision) { await detectAndUseNode(previewId, sshSession); const envVars = repoConfig.envVars; const envContent = Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join("\n"); await runSshStep(previewId, sshSession, `cat > /opt/app/.env << 'PPEOF' ${envContent} PPEOF`); if (isFirstProvision) { for (const cmd of repoConfig.setupCommands) { await runSshStep(previewId, sshSession, `cd /opt/app && ${cmd}`); } } await updateStatus(previewId, "BUILDING"); if (repoConfig.useDockerCompose) { const composePath = repoConfig.composeFilePath || "docker-compose.yml"; await runSshStep(previewId, sshSession, `cd /opt/app && docker compose -f ${composePath} up -d --build --force-recreate 2>&1`); } else { for (const cmd of repoConfig.buildCommands) { await runSshStep(previewId, sshSession, `cd /opt/app && ${cmd}`); } for (const cmd of repoConfig.postBuildCommands) { await runSshStep(previewId, sshSession, `cd /opt/app && ${cmd}`); } if (repoConfig.runCommand) { const res = await runSshStep( previewId, sshSession, `cd /opt/app && nohup ${repoConfig.runCommand} > /opt/app/pp.log 2>&1 & echo $!` ); const pid = parseInt(res.stdout.trim(), 10); if (!isNaN(pid)) { await import_db.prisma.preview.update({ where: { id: previewId }, data: { pid } }); } } } } async function detectAndUseNode(previewId, sshSession) { const nvmSource = `export NVM_DIR="/root/.nvm" && source "$NVM_DIR/nvm.sh"`; const res = await runSshStep(previewId, sshSession, `${nvmSource} && [ -f /opt/app/.nvmrc ] && nvm install && nvm use || nvm use default 2>&1`, false); } async function runSshStep(previewId, sshSession, command, throwOnFail = true) { checkAbortSession(sshSession); await appendLog(previewId, `$ ${command} `); const res = await sshSession.exec(command); if (res.stdout) await appendLog(previewId, res.stdout); if (res.stderr) await appendLog(previewId, res.stderr); if (throwOnFail && res.code !== 0) { throw new Error(`Command failed with exit code ${res.code}: ${command}`); } return res; } function checkAbortSession(session) { if (session.aborted) throw new Error("ABORTED"); } const abortSignals = /* @__PURE__ */ new Set(); function signalAbort(previewId) { abortSignals.add(previewId); abortJobForPreview(previewId); } function checkAbort(previewId) { if (abortSignals.has(previewId)) { abortSignals.delete(previewId); throw new Error("ABORTED"); } } async function stopPreview(previewId, reason = "STOPPED") { const preview = await import_db.prisma.preview.findUnique({ where: { id: previewId }, include: { repoConfig: { include: { user: true } } } }); if (!preview) return; const user = preview.repoConfig.user; const repoConfig = preview.repoConfig; if (preview.instanceId) { const ec2 = (0, import_ec2.makeEc2Client)(user); try { await (0, import_ec2.deleteKeyPairAws)(ec2, `pp-preview-${previewId}`); } catch { } try { await (0, import_ec2.deleteSecurityGroupAws)(ec2, `pp-preview-${previewId}`); } catch { } try { await (0, import_ec2.terminateInstance)(ec2, preview.instanceId); } catch { } } await import_db.prisma.preview.update({ where: { id: previewId }, data: { status: reason, stoppedAt: /* @__PURE__ */ new Date(), sshPrivateKey: null, sshKeyName: null, instanceId: null } }); const stoppedBody = (0, import_gitea.buildPrCommentBody)({ owner: repoConfig.repoOwner, repo: repoConfig.repoName, prNumber: preview.prNumber, status: "\u26AB Stopped (inactivity timeout / PR closed / manual stop)", commitSha: preview.commitSha, updatedAt: /* @__PURE__ */ new Date(), ppBaseUrl: import_env.env.PP_BASE_URL }); if (preview.giteaCommentId) { try { await (0, import_gitea.updateComment)(user, repoConfig.repoOwner, repoConfig.repoName, preview.giteaCommentId, stoppedBody); } catch { } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { abortJobForPreview, appendLog, runDeploy, signalAbort, stopPreview, subscribeToLogs }); //# sourceMappingURL=deploy.js.map