137 lines
3.7 KiB
JavaScript
137 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* DashCaddy Release Webhook Handler
|
|
* Receives push webhooks from Gitea, verifies HMAC signature,
|
|
* and triggers build-release.sh.
|
|
*
|
|
* Usage: node webhook-handler.js
|
|
* Env vars:
|
|
* WEBHOOK_SECRET — Gitea webhook secret (required)
|
|
* WEBHOOK_PORT — Listen port (default: 9090)
|
|
* BUILD_SCRIPT — Path to build script (default: /opt/dashcaddy-release/build-release.sh)
|
|
*/
|
|
|
|
const http = require('http');
|
|
const crypto = require('crypto');
|
|
const { spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
|
|
const PORT = parseInt(process.env.WEBHOOK_PORT || '9090', 10);
|
|
const SECRET = process.env.WEBHOOK_SECRET;
|
|
const BUILD_SCRIPT = process.env.BUILD_SCRIPT || '/opt/dashcaddy-release/build-release.sh';
|
|
const LOG_FILE = '/var/log/dashcaddy-release.log';
|
|
|
|
if (!SECRET) {
|
|
console.error('WEBHOOK_SECRET environment variable is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
let buildRunning = false;
|
|
|
|
function log(msg) {
|
|
const line = `[webhook] ${new Date().toISOString()} ${msg}`;
|
|
console.log(line);
|
|
fs.appendFileSync(LOG_FILE, `${line }\n`);
|
|
}
|
|
|
|
function verifySignature(body, signature) {
|
|
if (!signature) return false;
|
|
const hmac = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
|
|
return crypto.timingSafeEqual(
|
|
Buffer.from(signature),
|
|
Buffer.from(hmac),
|
|
);
|
|
}
|
|
|
|
function triggerBuild() {
|
|
if (buildRunning) {
|
|
log('Build already in progress, skipping');
|
|
return;
|
|
}
|
|
buildRunning = true;
|
|
log('Triggering build...');
|
|
|
|
const child = spawn('bash', [BUILD_SCRIPT], {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: { ...process.env, PATH: process.env.PATH },
|
|
});
|
|
|
|
child.stdout.on('data', (data) => {
|
|
const lines = data.toString().trim().split('\n');
|
|
lines.forEach(line => log(`[build] ${line}`));
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
const lines = data.toString().trim().split('\n');
|
|
lines.forEach(line => log(`[build:err] ${line}`));
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
buildRunning = false;
|
|
if (code === 0) {
|
|
log('Build completed successfully');
|
|
} else {
|
|
log(`Build FAILED with exit code ${code}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
// Health check
|
|
if (req.method === 'GET' && req.url === '/health') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'ok', buildRunning }));
|
|
return;
|
|
}
|
|
|
|
// Only accept POST to /webhook
|
|
if (req.method !== 'POST' || req.url !== '/webhook') {
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
return;
|
|
}
|
|
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', () => {
|
|
// Verify Gitea HMAC signature
|
|
const sig = req.headers['x-gitea-signature'] || '';
|
|
if (!verifySignature(body, sig)) {
|
|
log('Signature verification FAILED');
|
|
res.writeHead(403);
|
|
res.end('Invalid signature');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const payload = JSON.parse(body);
|
|
const ref = payload.ref || '';
|
|
const branch = ref.replace('refs/heads/', '');
|
|
|
|
if (branch !== 'main') {
|
|
log(`Ignoring push to ${branch} (not main)`);
|
|
res.writeHead(200);
|
|
res.end('Ignored (not main branch)');
|
|
return;
|
|
}
|
|
|
|
const pusher = payload.pusher?.login || 'unknown';
|
|
const commits = payload.commits?.length || 0;
|
|
log(`Push to main by ${pusher}: ${commits} commit(s)`);
|
|
|
|
triggerBuild();
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ accepted: true }));
|
|
} catch (e) {
|
|
log(`Failed to parse webhook payload: ${ e.message}`);
|
|
res.writeHead(400);
|
|
res.end('Invalid payload');
|
|
}
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Webhook handler listening on 127.0.0.1:${PORT}`);
|
|
});
|