CI/CD: add deploy webhook endpoint for Gitea auto-deploy

This commit is contained in:
2026-03-29 13:23:07 +00:00
parent 4fae1c5a06
commit d0e464be5f
3 changed files with 133 additions and 64 deletions

View File

@@ -1,8 +1,14 @@
// Task Team — Deploy webhook — 2026-03-29
const { execSync } = require("child_process");
const { exec, execSync } = require("child_process");
const fs = require("fs");
const DEPLOY_SECRET = process.env.DEPLOY_SECRET || "taskteam-deploy-2026";
const EXEC_OPTS = { encoding: "utf8", env: { ...process.env, PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } };
const EXEC_ENV = { ...process.env, PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", HOME: "/root" };
const DEPLOY_LOG = "/opt/task-team/deploy.log";
function runCmd(cmd, timeout) {
return execSync(cmd, { encoding: "utf8", env: EXEC_ENV, timeout: timeout || 30000 });
}
async function deployRoutes(app) {
app.post("/deploy/webhook", async (req, reply) => {
@@ -19,40 +25,76 @@ async function deployRoutes(app) {
app.log.info("Deploy triggered by Gitea webhook");
try {
// Pull latest code
const pullResult = execSync("cd /opt/task-team && git pull origin master 2>&1", { ...EXEC_OPTS, timeout: 30000 });
// Run deploy asynchronously via shell script so we can return 200 immediately
const deployScript = `#!/bin/bash
set -e
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export HOME="/root"
LOG="${DEPLOY_LOG}"
echo "=== Deploy started at $(date -Is) ===" >> $LOG
// Install deps if package.json changed
execSync("cd /opt/task-team/api && npm install --production 2>&1", { ...EXEC_OPTS, timeout: 60000 });
# Pull latest code
cd /opt/task-team
git pull origin master >> $LOG 2>&1
// Reload API (zero-downtime)
execSync("pm2 reload taskteam-api 2>&1", { ...EXEC_OPTS, timeout: 15000 });
# Install deps
cd /opt/task-team/api
npm install --production >> $LOG 2>&1
// Build frontend
execSync("cd /opt/task-team/apps/tasks && NEXT_PUBLIC_API_URL=http://localhost:3000 npm run build 2>&1", { ...EXEC_OPTS, timeout: 120000 });
# Reload API (with --force to avoid "already in progress")
pm2 reload taskteam-api --force >> $LOG 2>&1
// Reload web
execSync("pm2 reload taskteam-web 2>&1", { ...EXEC_OPTS, timeout: 15000 });
# Build frontend (if it exists)
if [ -d /opt/task-team/apps/tasks ] && [ -f /opt/task-team/apps/tasks/package.json ]; then
cd /opt/task-team/apps/tasks
NEXT_PUBLIC_API_URL=http://localhost:3000 npm run build >> $LOG 2>&1
pm2 reload taskteam-web --force >> $LOG 2>&1 || true
fi
return { status: "deployed", pull: pullResult.trim().split("\n").slice(-3).join("\n") };
} catch (e) {
app.log.error(e, "Deploy failed");
return reply.code(500).send({ status: "failed", error: e.message });
}
echo "=== Deploy completed at $(date -Is) ===" >> $LOG
`;
// Write and execute deploy script in background
fs.writeFileSync("/opt/task-team/deploy.sh", deployScript, { mode: 0o755 });
exec("nohup /opt/task-team/deploy.sh &", { env: EXEC_ENV, detached: true });
return { status: "deploying", message: "Deploy started in background. Check /deploy/status for progress." };
});
// Manual deploy status
// Deploy status
app.get("/deploy/status", async () => {
try {
const commit = execSync("cd /opt/task-team && git log --oneline -1", { ...EXEC_OPTS }).trim();
const pm2 = execSync("pm2 jlist 2>/dev/null", { ...EXEC_OPTS });
const procs = JSON.parse(pm2).map(p => ({ name: p.name, status: p.pm2_env.status, uptime: p.pm2_env.pm_uptime }));
return { status: "ok", commit, processes: procs };
const commit = runCmd("cd /opt/task-team && git log --oneline -1").trim();
const pm2out = runCmd("pm2 jlist 2>/dev/null");
const procs = JSON.parse(pm2out).map(p => ({
name: p.name,
status: p.pm2_env.status,
uptime: p.pm2_env.pm_uptime,
restarts: p.pm2_env.restart_time
}));
// Last deploy log
let lastDeploy = "";
try {
const log = fs.readFileSync(DEPLOY_LOG, "utf8");
const lines = log.trim().split("\n");
lastDeploy = lines.slice(-10).join("\n");
} catch (_) {}
return { status: "ok", commit, processes: procs, last_deploy: lastDeploy };
} catch (e) {
return { status: "error", message: e.message };
}
});
// Manual trigger (GET for convenience)
app.post("/deploy/trigger", async (req, reply) => {
const secret = req.headers["x-deploy-secret"] || (req.body && req.body.secret);
if (secret !== DEPLOY_SECRET) {
return reply.code(401).send({ error: "invalid secret" });
}
// Simulate a push to master
req.body = { ref: "refs/heads/master", secret: DEPLOY_SECRET };
req.headers["x-gitea-secret"] = DEPLOY_SECRET;
return app.inject({ method: "POST", url: "/api/v1/deploy/webhook", payload: req.body, headers: { "x-gitea-secret": DEPLOY_SECRET } }).then(r => JSON.parse(r.body));
});
}
module.exports = deployRoutes;