feat: initial scaffold - backend, frontend, Prisma schema, Docker
- Prisma schema: User, Session, RepoConfig, Preview, Job, WebhookToken, NoConfigComment, AdminSettings - Backend: auth (login/logout/me/first-user setup), webhook handler with HMAC verification, EC2 service, SSH service, deploy pipeline, job queue worker, cron workers - Frontend: Login with first-user detection, Dashboard, PreviewDetail with live log streaming, Settings, Repos config, Admin panel, SetupWizard, Privacy page - Docker Compose and Dockerfile for self-hosted deployment - Uses bcryptjs for Node 24 compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+440
@@ -0,0 +1,440 @@
|
||||
"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
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+249
@@ -0,0 +1,249 @@
|
||||
"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 ec2_exports = {};
|
||||
__export(ec2_exports, {
|
||||
createPreviewSecurityGroup: () => createPreviewSecurityGroup,
|
||||
deleteKeyPairAws: () => deleteKeyPairAws,
|
||||
deleteSecurityGroupAws: () => deleteSecurityGroupAws,
|
||||
describeAllManagedInstances: () => describeAllManagedInstances,
|
||||
generateAndImportKeyPair: () => generateAndImportKeyPair,
|
||||
generateSshKeyPair: () => generateSshKeyPair,
|
||||
launchInstance: () => launchInstance,
|
||||
makeEc2Client: () => makeEc2Client,
|
||||
makeStsClient: () => makeStsClient,
|
||||
terminateInstance: () => terminateInstance,
|
||||
validateAwsCredentials: () => validateAwsCredentials,
|
||||
waitForInstanceRunning: () => waitForInstanceRunning
|
||||
});
|
||||
module.exports = __toCommonJS(ec2_exports);
|
||||
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
||||
var import_client_sts = require("@aws-sdk/client-sts");
|
||||
var import_crypto = require("crypto");
|
||||
var import_logger = require("../lib/logger");
|
||||
var import_encryption = require("../lib/encryption");
|
||||
const log = (0, import_logger.createLogger)("EC2");
|
||||
const UBUNTU_22_04_AMI = {
|
||||
"us-east-1": "ami-0e86e20dae9224db8",
|
||||
"us-east-2": "ami-0a0d9cf81c479446a",
|
||||
"us-west-1": "ami-05c969369880fa2c2",
|
||||
"us-west-2": "ami-03f8acd418785369b",
|
||||
"eu-west-1": "ami-0694d931cee176e7d",
|
||||
"eu-west-2": "ami-0f3d9639a5674d559",
|
||||
"eu-west-3": "ami-022e307f4b9e39f45",
|
||||
"eu-central-1": "ami-0faab6bdbac9486fb",
|
||||
"ap-southeast-1": "ami-0823c236601fef765",
|
||||
"ap-southeast-2": "ami-07620139298af599e",
|
||||
"ap-northeast-1": "ami-0b7546e839d7ace12",
|
||||
"ap-northeast-2": "ami-042e76978adeb8c48",
|
||||
"ap-south-1": "ami-076e3a557efe1aa9c",
|
||||
"sa-east-1": "ami-0eed58016fbe42de3",
|
||||
"ca-central-1": "ami-024f768de9e73d4f4",
|
||||
"eu-north-1": "ami-00381a880aa48c6c6",
|
||||
"me-south-1": "ami-09574f34b8dcd2eac",
|
||||
"af-south-1": "ami-08fdcf06b39fe83ec"
|
||||
};
|
||||
function makeEc2Client(user) {
|
||||
const accessKeyId = user.awsAccessKeyId ? (0, import_encryption.decrypt)(user.awsAccessKeyId) : "";
|
||||
const secretAccessKey = user.awsSecretAccessKey ? (0, import_encryption.decrypt)(user.awsSecretAccessKey) : "";
|
||||
return new import_client_ec2.EC2Client({
|
||||
region: user.awsRegion,
|
||||
credentials: { accessKeyId, secretAccessKey }
|
||||
});
|
||||
}
|
||||
function makeStsClient(user) {
|
||||
const accessKeyId = user.awsAccessKeyId ? (0, import_encryption.decrypt)(user.awsAccessKeyId) : "";
|
||||
const secretAccessKey = user.awsSecretAccessKey ? (0, import_encryption.decrypt)(user.awsSecretAccessKey) : "";
|
||||
return new import_client_sts.STSClient({
|
||||
region: user.awsRegion,
|
||||
credentials: { accessKeyId, secretAccessKey }
|
||||
});
|
||||
}
|
||||
async function validateAwsCredentials(user) {
|
||||
try {
|
||||
const sts = makeStsClient(user);
|
||||
const res = await sts.send(new import_client_sts.GetCallerIdentityCommand({}));
|
||||
return { success: true, arn: res.Arn };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
function generateSshKeyPair() {
|
||||
const { privateKey, publicKey } = (0, import_crypto.generateKeyPairSync)("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs1", format: "pem" }
|
||||
});
|
||||
const pubKeyOpenSsh = rsaPemToOpenSsh(publicKey);
|
||||
return { privateKey, publicKey: pubKeyOpenSsh };
|
||||
}
|
||||
function rsaPemToOpenSsh(pem) {
|
||||
const { publicKeyEncoding } = (0, import_crypto.generateKeyPairSync)("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: "pkcs8", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
});
|
||||
void publicKeyEncoding;
|
||||
const der = Buffer.from(
|
||||
pem.replace(/-----BEGIN RSA PUBLIC KEY-----/, "").replace(/-----END RSA PUBLIC KEY-----/, "").replace(/\n/g, ""),
|
||||
"base64"
|
||||
);
|
||||
const type = Buffer.from("ssh-rsa");
|
||||
function encodeBuffer(buf) {
|
||||
const len = Buffer.allocUnsafe(4);
|
||||
len.writeUInt32BE(buf.length, 0);
|
||||
return Buffer.concat([len, buf]);
|
||||
}
|
||||
const typeEncoded = encodeBuffer(type);
|
||||
const rsaKeyData = der;
|
||||
const base64Key = Buffer.concat([typeEncoded, rsaKeyData]).toString("base64");
|
||||
return `ssh-rsa ${base64Key} pp-generated`;
|
||||
}
|
||||
async function generateAndImportKeyPair(ec2, keyName) {
|
||||
const { privateKey, publicKey } = generateSshKeyPair();
|
||||
await ec2.send(new import_client_ec2.ImportKeyPairCommand({
|
||||
KeyName: keyName,
|
||||
PublicKeyMaterial: Buffer.from(publicKey)
|
||||
}));
|
||||
return { privateKey };
|
||||
}
|
||||
async function createPreviewSecurityGroup(ec2, groupName, port) {
|
||||
const describe = await ec2.send(new import_client_ec2.DescribeSecurityGroupsCommand({
|
||||
Filters: [{ Name: "group-name", Values: [groupName] }]
|
||||
}));
|
||||
if (describe.SecurityGroups && describe.SecurityGroups.length > 0) {
|
||||
return describe.SecurityGroups[0].GroupId;
|
||||
}
|
||||
const res = await ec2.send(new import_client_ec2.CreateSecurityGroupCommand({
|
||||
GroupName: groupName,
|
||||
Description: `PP Preview security group: ${groupName}`
|
||||
}));
|
||||
const groupId = res.GroupId;
|
||||
await ec2.send(new import_client_ec2.AuthorizeSecurityGroupIngressCommand({
|
||||
GroupId: groupId,
|
||||
IpPermissions: [
|
||||
{
|
||||
IpProtocol: "tcp",
|
||||
FromPort: 22,
|
||||
ToPort: 22,
|
||||
IpRanges: [{ CidrIp: "0.0.0.0/0" }]
|
||||
},
|
||||
{
|
||||
IpProtocol: "tcp",
|
||||
FromPort: port,
|
||||
ToPort: port,
|
||||
IpRanges: [{ CidrIp: "0.0.0.0/0" }]
|
||||
}
|
||||
]
|
||||
}));
|
||||
return groupId;
|
||||
}
|
||||
const BOOTSTRAP_SCRIPT = `#!/bin/bash
|
||||
set -e
|
||||
apt-get update -y
|
||||
apt-get install -y curl git unzip build-essential
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
apt-get install -y docker-compose-plugin
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
export NVM_DIR="/root/.nvm"
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
nvm install --lts
|
||||
nvm alias default lts/*
|
||||
`;
|
||||
async function launchInstance(opts) {
|
||||
const ami = UBUNTU_22_04_AMI[opts.region] ?? UBUNTU_22_04_AMI["us-east-1"];
|
||||
const tagSpecs = Object.entries(opts.tags).map(([k, v]) => ({ Key: k, Value: v }));
|
||||
tagSpecs.push({ Key: "Name", Value: `pp-preview-${opts.tags["pp:previewId"]}` });
|
||||
const res = await opts.ec2.send(new import_client_ec2.RunInstancesCommand({
|
||||
ImageId: ami,
|
||||
InstanceType: opts.instanceType,
|
||||
MinCount: 1,
|
||||
MaxCount: 1,
|
||||
KeyName: opts.keyName,
|
||||
SecurityGroupIds: [opts.securityGroupId],
|
||||
UserData: Buffer.from(BOOTSTRAP_SCRIPT).toString("base64"),
|
||||
TagSpecifications: [
|
||||
{ ResourceType: "instance", Tags: tagSpecs }
|
||||
]
|
||||
}));
|
||||
return res.Instances[0].InstanceId;
|
||||
}
|
||||
async function waitForInstanceRunning(ec2, instanceId, maxWaitMs = 3e5) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxWaitMs) {
|
||||
const res = await ec2.send(new import_client_ec2.DescribeInstancesCommand({
|
||||
InstanceIds: [instanceId]
|
||||
}));
|
||||
const inst = res.Reservations?.[0]?.Instances?.[0];
|
||||
if (inst?.State?.Name === "running" && inst.PublicIpAddress) {
|
||||
return inst.PublicIpAddress;
|
||||
}
|
||||
await sleep(5e3);
|
||||
}
|
||||
throw new Error(`Instance ${instanceId} did not reach running state within timeout`);
|
||||
}
|
||||
async function terminateInstance(ec2, instanceId) {
|
||||
await ec2.send(new import_client_ec2.TerminateInstancesCommand({ InstanceIds: [instanceId] }));
|
||||
}
|
||||
async function deleteKeyPairAws(ec2, keyName) {
|
||||
try {
|
||||
await ec2.send(new import_client_ec2.DeleteKeyPairCommand({ KeyName: keyName }));
|
||||
} catch (e) {
|
||||
log.warn({ e, keyName }, "Failed to delete key pair");
|
||||
}
|
||||
}
|
||||
async function deleteSecurityGroupAws(ec2, groupName) {
|
||||
try {
|
||||
const describe = await ec2.send(new import_client_ec2.DescribeSecurityGroupsCommand({
|
||||
Filters: [{ Name: "group-name", Values: [groupName] }]
|
||||
}));
|
||||
const groupId = describe.SecurityGroups?.[0]?.GroupId;
|
||||
if (groupId) {
|
||||
await ec2.send(new import_client_ec2.DeleteSecurityGroupCommand({ GroupId: groupId }));
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn({ e, groupName }, "Failed to delete security group");
|
||||
}
|
||||
}
|
||||
async function describeAllManagedInstances(ec2) {
|
||||
const res = await ec2.send(new import_client_ec2.DescribeInstancesCommand({
|
||||
Filters: [{ Name: "tag:pp:managed", Values: ["true"] }, { Name: "instance-state-name", Values: ["running", "pending", "stopping", "stopped"] }]
|
||||
}));
|
||||
return (res.Reservations ?? []).flatMap((r) => r.Instances ?? []);
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
createPreviewSecurityGroup,
|
||||
deleteKeyPairAws,
|
||||
deleteSecurityGroupAws,
|
||||
describeAllManagedInstances,
|
||||
generateAndImportKeyPair,
|
||||
generateSshKeyPair,
|
||||
launchInstance,
|
||||
makeEc2Client,
|
||||
makeStsClient,
|
||||
terminateInstance,
|
||||
validateAwsCredentials,
|
||||
waitForInstanceRunning
|
||||
});
|
||||
//# sourceMappingURL=ec2.js.map
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var gitea_exports = {};
|
||||
__export(gitea_exports, {
|
||||
buildPrCommentBody: () => buildPrCommentBody,
|
||||
checkUserPermission: () => checkUserPermission,
|
||||
deleteWebhook: () => deleteWebhook,
|
||||
fetchUserRepos: () => fetchUserRepos,
|
||||
getRepoCollaboratorPermission: () => getRepoCollaboratorPermission,
|
||||
giteaApi: () => giteaApi,
|
||||
postComment: () => postComment,
|
||||
registerWebhook: () => registerWebhook,
|
||||
updateComment: () => updateComment,
|
||||
updateWebhookSecret: () => updateWebhookSecret,
|
||||
validateGiteaUrl: () => validateGiteaUrl
|
||||
});
|
||||
module.exports = __toCommonJS(gitea_exports);
|
||||
var import_axios = __toESM(require("axios"));
|
||||
var import_encryption = require("../lib/encryption");
|
||||
function giteaApi(user) {
|
||||
const pat = user.giteaPAT ? (0, import_encryption.decrypt)(user.giteaPAT) : "";
|
||||
return import_axios.default.create({
|
||||
baseURL: `${user.giteaInstanceUrl}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `token ${pat}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
timeout: 15e3
|
||||
});
|
||||
}
|
||||
async function validateGiteaUrl(url) {
|
||||
try {
|
||||
const res = await import_axios.default.get(`${url}/api/v1/version`, { timeout: 1e4 });
|
||||
return { success: true, version: res.data.version };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
async function fetchUserRepos(user) {
|
||||
const api = giteaApi(user);
|
||||
const repos = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const res = await api.get(`/repos/search?limit=50&page=${page}`);
|
||||
const data = res.data?.data ?? [];
|
||||
if (data.length === 0) break;
|
||||
repos.push(...data);
|
||||
if (data.length < 50) break;
|
||||
page++;
|
||||
}
|
||||
return repos;
|
||||
}
|
||||
async function registerWebhook(user, owner, repo, webhookUrl, secret) {
|
||||
const api = giteaApi(user);
|
||||
const res = await api.post(`/repos/${owner}/${repo}/hooks`, {
|
||||
type: "gitea",
|
||||
config: {
|
||||
url: webhookUrl,
|
||||
secret,
|
||||
content_type: "json"
|
||||
},
|
||||
events: ["pull_request", "issue_comment"],
|
||||
active: true
|
||||
});
|
||||
return res.data.id;
|
||||
}
|
||||
async function deleteWebhook(user, owner, repo, hookId) {
|
||||
const api = giteaApi(user);
|
||||
await api.delete(`/repos/${owner}/${repo}/hooks/${hookId}`);
|
||||
}
|
||||
async function updateWebhookSecret(user, owner, repo, hookId, webhookUrl, newSecret) {
|
||||
const api = giteaApi(user);
|
||||
await api.patch(`/repos/${owner}/${repo}/hooks/${hookId}`, {
|
||||
config: {
|
||||
url: webhookUrl,
|
||||
secret: newSecret,
|
||||
content_type: "json"
|
||||
},
|
||||
events: ["pull_request", "issue_comment"],
|
||||
active: true
|
||||
});
|
||||
}
|
||||
async function postComment(user, owner, repo, issueNumber, body) {
|
||||
const api = giteaApi(user);
|
||||
const res = await api.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { body });
|
||||
return res.data.id;
|
||||
}
|
||||
async function updateComment(user, owner, repo, commentId, body) {
|
||||
const api = giteaApi(user);
|
||||
await api.patch(`/repos/${owner}/${repo}/issues/comments/${commentId}`, { body });
|
||||
}
|
||||
async function checkUserPermission(user, owner, repo, username) {
|
||||
try {
|
||||
const api = giteaApi(user);
|
||||
const res = await api.get(`/repos/${owner}/${repo}/collaborators/${username}`);
|
||||
return res.status === 204;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function getRepoCollaboratorPermission(user, owner, repo, username) {
|
||||
try {
|
||||
const api = giteaApi(user);
|
||||
const res = await api.get(`/repos/${owner}/${repo}/collaborators/${username}/permission`);
|
||||
return res.data?.permission ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function buildPrCommentBody(opts) {
|
||||
const { owner, repo, prNumber, status, commitSha, updatedAt, ppBaseUrl, lastLogLines, instanceIp, port } = opts;
|
||||
const ts = updatedAt.toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
||||
let statusLine = status;
|
||||
if (lastLogLines) {
|
||||
statusLine += `
|
||||
|
||||
\`\`\`
|
||||
${lastLogLines}
|
||||
\`\`\``;
|
||||
}
|
||||
return `## \u{1F680} PR Preview \u2014 \`${owner}/${repo}\` #${prNumber}
|
||||
|
||||
**Status:** ${statusLine}
|
||||
**Commit:** \`${commitSha.slice(0, 8)}\`
|
||||
**Updated:** ${ts}
|
||||
|
||||
---
|
||||
_Powered by [PR Previews](${ppBaseUrl})_`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
buildPrCommentBody,
|
||||
checkUserPermission,
|
||||
deleteWebhook,
|
||||
fetchUserRepos,
|
||||
getRepoCollaboratorPermission,
|
||||
giteaApi,
|
||||
postComment,
|
||||
registerWebhook,
|
||||
updateComment,
|
||||
updateWebhookSecret,
|
||||
validateGiteaUrl
|
||||
});
|
||||
//# sourceMappingURL=gitea.js.map
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../src/services/gitea.ts"],
|
||||
"sourcesContent": ["import axios from \"axios\";\nimport { decrypt } from \"../lib/encryption\";\nimport type { User } from \"@prisma/client\";\n\nexport function giteaApi(user: User) {\n const pat = user.giteaPAT ? decrypt(user.giteaPAT) : \"\";\n return axios.create({\n baseURL: `${user.giteaInstanceUrl}/api/v1`,\n headers: {\n Authorization: `token ${pat}`,\n \"Content-Type\": \"application/json\",\n },\n timeout: 15000,\n });\n}\n\nexport async function validateGiteaUrl(url: string): Promise<{ success: boolean; version?: string; error?: string }> {\n try {\n const res = await axios.get(`${url}/api/v1/version`, { timeout: 10000 });\n return { success: true, version: res.data.version };\n } catch (e: any) {\n return { success: false, error: e.message };\n }\n}\n\nexport async function fetchUserRepos(user: User): Promise<any[]> {\n const api = giteaApi(user);\n const repos: any[] = [];\n let page = 1;\n while (true) {\n const res = await api.get(`/repos/search?limit=50&page=${page}`);\n const data = res.data?.data ?? [];\n if (data.length === 0) break;\n repos.push(...data);\n if (data.length < 50) break;\n page++;\n }\n return repos;\n}\n\nexport async function registerWebhook(user: User, owner: string, repo: string, webhookUrl: string, secret: string): Promise<number> {\n const api = giteaApi(user);\n const res = await api.post(`/repos/${owner}/${repo}/hooks`, {\n type: \"gitea\",\n config: {\n url: webhookUrl,\n secret,\n content_type: \"json\",\n },\n events: [\"pull_request\", \"issue_comment\"],\n active: true,\n });\n return res.data.id;\n}\n\nexport async function deleteWebhook(user: User, owner: string, repo: string, hookId: string): Promise<void> {\n const api = giteaApi(user);\n await api.delete(`/repos/${owner}/${repo}/hooks/${hookId}`);\n}\n\nexport async function updateWebhookSecret(user: User, owner: string, repo: string, hookId: string, webhookUrl: string, newSecret: string): Promise<void> {\n const api = giteaApi(user);\n await api.patch(`/repos/${owner}/${repo}/hooks/${hookId}`, {\n config: {\n url: webhookUrl,\n secret: newSecret,\n content_type: \"json\",\n },\n events: [\"pull_request\", \"issue_comment\"],\n active: true,\n });\n}\n\nexport async function postComment(user: User, owner: string, repo: string, issueNumber: number, body: string): Promise<number> {\n const api = giteaApi(user);\n const res = await api.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { body });\n return res.data.id;\n}\n\nexport async function updateComment(user: User, owner: string, repo: string, commentId: number, body: string): Promise<void> {\n const api = giteaApi(user);\n await api.patch(`/repos/${owner}/${repo}/issues/comments/${commentId}`, { body });\n}\n\nexport async function checkUserPermission(user: User, owner: string, repo: string, username: string): Promise<boolean> {\n try {\n const api = giteaApi(user);\n const res = await api.get(`/repos/${owner}/${repo}/collaborators/${username}`);\n return res.status === 204;\n } catch {\n return false;\n }\n}\n\nexport async function getRepoCollaboratorPermission(user: User, owner: string, repo: string, username: string): Promise<string | null> {\n try {\n const api = giteaApi(user);\n const res = await api.get(`/repos/${owner}/${repo}/collaborators/${username}/permission`);\n return res.data?.permission ?? null;\n } catch {\n return null;\n }\n}\n\nexport function buildPrCommentBody(opts: {\n owner: string;\n repo: string;\n prNumber: number;\n status: string;\n commitSha: string;\n updatedAt: Date;\n ppBaseUrl: string;\n lastLogLines?: string;\n instanceIp?: string;\n port?: number;\n}): string {\n const { owner, repo, prNumber, status, commitSha, updatedAt, ppBaseUrl, lastLogLines, instanceIp, port } = opts;\n const ts = updatedAt.toISOString().replace(\"T\", \" \").slice(0, 19) + \" UTC\";\n\n let statusLine = status;\n if (lastLogLines) {\n statusLine += `\\n\\n\\`\\`\\`\\n${lastLogLines}\\n\\`\\`\\``;\n }\n\n return `## \uD83D\uDE80 PR Preview \u2014 \\`${owner}/${repo}\\` #${prNumber}\n\n**Status:** ${statusLine}\n**Commit:** \\`${commitSha.slice(0, 8)}\\`\n**Updated:** ${ts}\n\n---\n_Powered by [PR Previews](${ppBaseUrl})_`;\n}\n"],
|
||||
"mappings": "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",
|
||||
"names": ["axios"]
|
||||
}
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
"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 ssh_exports = {};
|
||||
__export(ssh_exports, {
|
||||
connectSsh: () => connectSsh
|
||||
});
|
||||
module.exports = __toCommonJS(ssh_exports);
|
||||
var import_ssh2 = require("ssh2");
|
||||
var import_logger = require("../lib/logger");
|
||||
const log = (0, import_logger.createLogger)("SSH");
|
||||
async function connectSsh(host, privateKey, maxWaitMs = 3e5) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxWaitMs) {
|
||||
try {
|
||||
const conn = await tryConnect(host, privateKey, 1e4);
|
||||
let aborted = false;
|
||||
return {
|
||||
get aborted() {
|
||||
return aborted;
|
||||
},
|
||||
abort() {
|
||||
aborted = true;
|
||||
try {
|
||||
conn.end();
|
||||
} catch {
|
||||
}
|
||||
},
|
||||
async exec(command) {
|
||||
if (aborted) throw new Error("SSH session aborted");
|
||||
return execOnConn(conn, command);
|
||||
},
|
||||
close() {
|
||||
try {
|
||||
conn.end();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
if (Date.now() - start > maxWaitMs) throw e;
|
||||
log.debug({ host, error: e.message }, "SSH connect retry");
|
||||
await sleep(5e3);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not SSH into ${host} within timeout`);
|
||||
}
|
||||
function tryConnect(host, privateKey, timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new import_ssh2.Client();
|
||||
const timer = setTimeout(() => {
|
||||
conn.end();
|
||||
reject(new Error(`SSH connection to ${host} timed out`));
|
||||
}, timeoutMs);
|
||||
conn.on("ready", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(conn);
|
||||
});
|
||||
conn.on("error", (e) => {
|
||||
clearTimeout(timer);
|
||||
reject(e);
|
||||
});
|
||||
conn.connect({
|
||||
host,
|
||||
port: 22,
|
||||
username: "ubuntu",
|
||||
privateKey,
|
||||
readyTimeout: timeoutMs,
|
||||
algorithms: {
|
||||
serverHostKey: ["ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"]
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function execOnConn(conn, command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
stream.on("data", (d) => {
|
||||
stdout += d.toString();
|
||||
});
|
||||
stream.stderr.on("data", (d) => {
|
||||
stderr += d.toString();
|
||||
});
|
||||
stream.on("close", (code) => {
|
||||
resolve({ stdout, stderr, code: code ?? 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
connectSsh
|
||||
});
|
||||
//# sourceMappingURL=ssh.js.map
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../src/services/ssh.ts"],
|
||||
"sourcesContent": ["import { Client } from \"ssh2\";\nimport { createLogger } from \"../lib/logger\";\n\nconst log = createLogger(\"SSH\");\n\nexport interface SshSession {\n exec(command: string): Promise<{ stdout: string; stderr: string; code: number }>;\n close(): void;\n aborted: boolean;\n abort(): void;\n}\n\nexport async function connectSsh(host: string, privateKey: string, maxWaitMs = 300_000): Promise<SshSession> {\n const start = Date.now();\n\n while (Date.now() - start < maxWaitMs) {\n try {\n const conn = await tryConnect(host, privateKey, 10000);\n let aborted = false;\n\n return {\n get aborted() { return aborted; },\n abort() {\n aborted = true;\n try { conn.end(); } catch {}\n },\n async exec(command: string) {\n if (aborted) throw new Error(\"SSH session aborted\");\n return execOnConn(conn, command);\n },\n close() {\n try { conn.end(); } catch {}\n },\n };\n } catch (e: any) {\n if (Date.now() - start > maxWaitMs) throw e;\n log.debug({ host, error: e.message }, \"SSH connect retry\");\n await sleep(5000);\n }\n }\n throw new Error(`Could not SSH into ${host} within timeout`);\n}\n\nfunction tryConnect(host: string, privateKey: string, timeoutMs: number): Promise<Client> {\n return new Promise((resolve, reject) => {\n const conn = new Client();\n const timer = setTimeout(() => {\n conn.end();\n reject(new Error(`SSH connection to ${host} timed out`));\n }, timeoutMs);\n\n conn.on(\"ready\", () => {\n clearTimeout(timer);\n resolve(conn);\n });\n conn.on(\"error\", (e) => {\n clearTimeout(timer);\n reject(e);\n });\n conn.connect({\n host,\n port: 22,\n username: \"ubuntu\",\n privateKey,\n readyTimeout: timeoutMs,\n algorithms: {\n serverHostKey: [\"ssh-rsa\", \"ecdsa-sha2-nistp256\", \"ecdsa-sha2-nistp384\", \"ecdsa-sha2-nistp521\"],\n },\n });\n });\n}\n\nfunction execOnConn(conn: Client, command: string): Promise<{ stdout: string; stderr: string; code: number }> {\n return new Promise((resolve, reject) => {\n conn.exec(command, (err, stream) => {\n if (err) return reject(err);\n let stdout = \"\";\n let stderr = \"\";\n stream.on(\"data\", (d: Buffer) => { stdout += d.toString(); });\n stream.stderr.on(\"data\", (d: Buffer) => { stderr += d.toString(); });\n stream.on(\"close\", (code: number) => {\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n });\n}\n\nfunction sleep(ms: number) {\n return new Promise(r => setTimeout(r, ms));\n}\n"],
|
||||
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAuB;AACvB,oBAA6B;AAE7B,MAAM,UAAM,4BAAa,KAAK;AAS9B,eAAsB,WAAW,MAAc,YAAoB,YAAY,KAA8B;AAC3G,QAAM,QAAQ,KAAK,IAAI;AAEvB,SAAO,KAAK,IAAI,IAAI,QAAQ,WAAW;AACrC,QAAI;AACF,YAAM,OAAO,MAAM,WAAW,MAAM,YAAY,GAAK;AACrD,UAAI,UAAU;AAEd,aAAO;AAAA,QACL,IAAI,UAAU;AAAE,iBAAO;AAAA,QAAS;AAAA,QAChC,QAAQ;AACN,oBAAU;AACV,cAAI;AAAE,iBAAK,IAAI;AAAA,UAAG,QAAQ;AAAA,UAAC;AAAA,QAC7B;AAAA,QACA,MAAM,KAAK,SAAiB;AAC1B,cAAI,QAAS,OAAM,IAAI,MAAM,qBAAqB;AAClD,iBAAO,WAAW,MAAM,OAAO;AAAA,QACjC;AAAA,QACA,QAAQ;AACN,cAAI;AAAE,iBAAK,IAAI;AAAA,UAAG,QAAQ;AAAA,UAAC;AAAA,QAC7B;AAAA,MACF;AAAA,IACF,SAAS,GAAQ;AACf,UAAI,KAAK,IAAI,IAAI,QAAQ,UAAW,OAAM;AAC1C,UAAI,MAAM,EAAE,MAAM,OAAO,EAAE,QAAQ,GAAG,mBAAmB;AACzD,YAAM,MAAM,GAAI;AAAA,IAClB;AAAA,EACF;AACA,QAAM,IAAI,MAAM,sBAAsB,IAAI,iBAAiB;AAC7D;AAEA,SAAS,WAAW,MAAc,YAAoB,WAAoC;AACxF,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,OAAO,IAAI,mBAAO;AACxB,UAAM,QAAQ,WAAW,MAAM;AAC7B,WAAK,IAAI;AACT,aAAO,IAAI,MAAM,qBAAqB,IAAI,YAAY,CAAC;AAAA,IACzD,GAAG,SAAS;AAEZ,SAAK,GAAG,SAAS,MAAM;AACrB,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,MAAM;AACtB,mBAAa,KAAK;AAClB,aAAO,CAAC;AAAA,IACV,CAAC;AACD,SAAK,QAAQ;AAAA,MACX;AAAA,MACA,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,MACA,cAAc;AAAA,MACd,YAAY;AAAA,QACV,eAAe,CAAC,WAAW,uBAAuB,uBAAuB,qBAAqB;AAAA,MAChG;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,WAAW,MAAc,SAA4E;AAC5G,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,SAAK,KAAK,SAAS,CAAC,KAAK,WAAW;AAClC,UAAI,IAAK,QAAO,OAAO,GAAG;AAC1B,UAAI,SAAS;AACb,UAAI,SAAS;AACb,aAAO,GAAG,QAAQ,CAAC,MAAc;AAAE,kBAAU,EAAE,SAAS;AAAA,MAAG,CAAC;AAC5D,aAAO,OAAO,GAAG,QAAQ,CAAC,MAAc;AAAE,kBAAU,EAAE,SAAS;AAAA,MAAG,CAAC;AACnE,aAAO,GAAG,SAAS,CAAC,SAAiB;AACnC,gBAAQ,EAAE,QAAQ,QAAQ,MAAM,QAAQ,EAAE,CAAC;AAAA,MAC7C,CAAC;AAAA,IACH,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,MAAM,IAAY;AACzB,SAAO,IAAI,QAAQ,OAAK,WAAW,GAAG,EAAE,CAAC;AAC3C;",
|
||||
"names": []
|
||||
}
|
||||
Reference in New Issue
Block a user