/** * CodeUp / GitLab 兼容的部署 Webhook 接收器 * 监听 127.0.0.1:8095,由 Nginx 反代 /hooks/deploy */ import http from 'http'; import { spawn } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); function loadEnv() { const envPath = join(__dirname, 'auto-release.env'); if (!existsSync(envPath)) return; for (const line of readFileSync(envPath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq === -1) continue; const key = trimmed.slice(0, eq).trim(); let val = trimmed.slice(eq + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (!(key in process.env)) process.env[key] = val; } } loadEnv(); const PORT = Number(process.env.DEPLOY_WEBHOOK_PORT || 8095); const HOST = process.env.DEPLOY_WEBHOOK_HOST || '127.0.0.1'; const SECRET = process.env.DEPLOY_WEBHOOK_SECRET || ''; const ALLOWED_REF = process.env.DEPLOY_GIT_REF || 'refs/heads/dev'; const APP_ROOT = process.env.APP_ROOT || '/opt/dukang-haoke'; const RELEASE_SCRIPT = join(APP_ROOT, 'deploy', 'auto-release.sh'); function readBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); req.on('error', reject); }); } function getToken(req) { return ( req.headers['x-gitlab-token'] || req.headers['x-codeup-token'] || req.headers['x-deploy-token'] || '' ); } function json(res, status, data) { const body = JSON.stringify(data); res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(body); } function triggerRelease(trigger) { const child = spawn('bash', [RELEASE_SCRIPT], { detached: true, stdio: 'ignore', env: { ...process.env, DEPLOY_TRIGGER: trigger }, }); child.unref(); } const server = http.createServer(async (req, res) => { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); if (req.method === 'GET' && url.pathname === '/health') { return json(res, 200, { ok: true, service: 'dukang-deploy-webhook' }); } if (req.method !== 'POST' || url.pathname !== '/deploy') { return json(res, 404, { ok: false, message: 'not found' }); } if (!SECRET) { return json(res, 503, { ok: false, message: 'webhook secret not configured' }); } const token = getToken(req); if (token !== SECRET) { return json(res, 403, { ok: false, message: 'invalid token' }); } let payload = {}; try { const raw = await readBody(req); if (raw) payload = JSON.parse(raw); } catch { return json(res, 400, { ok: false, message: 'invalid json body' }); } const ref = payload.ref || payload.object_attributes?.ref || ''; if (ref && ref !== ALLOWED_REF) { return json(res, 200, { ok: true, skipped: true, message: `ignored ref: ${ref} (allowed: ${ALLOWED_REF})`, }); } triggerRelease(ref || 'manual'); return json(res, 202, { ok: true, accepted: true, message: 'deploy started', ref: ref || ALLOWED_REF, }); }); server.listen(PORT, HOST, () => { console.log(`dukang deploy webhook listening on http://${HOST}:${PORT}`); });