Replit-Commit-Author: Deployment Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: bca5769b-f33f-4202-85e3-b4f84e426350 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/90c7a60b-2c61-4699-b5c6-6a1ac7469a4d/G03TLre Replit-Commit-Deployment-Build-Id: 6750cd6c-5980-4b2b-bcd1-ceb093d94078 Replit-Helium-Checkpoint-Created: true
115 lines
3.5 KiB
JavaScript
115 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// =============================================================================
|
|
// /opt/timmy-tower/webhook.js
|
|
// Lightweight webhook receiver: validates Gitea HMAC signature, runs deploy.sh
|
|
// Runs as systemd service: timmy-deploy-hook
|
|
// =============================================================================
|
|
'use strict';
|
|
|
|
const http = require('http');
|
|
const crypto = require('crypto');
|
|
const { execFile } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PORT = 9000;
|
|
const HOST = '127.0.0.1';
|
|
const SECRET = process.env.WEBHOOK_SECRET || '';
|
|
const DEPLOY_SCRIPT = path.join(__dirname, 'deploy.sh');
|
|
const LOG_FILE = path.join(__dirname, 'deploy.log');
|
|
|
|
let deploying = false;
|
|
|
|
function log(msg) {
|
|
const line = `[${new Date().toISOString()}] [webhook] ${msg}\n`;
|
|
process.stdout.write(line);
|
|
try { fs.appendFileSync(LOG_FILE, line); } catch (_) {}
|
|
}
|
|
|
|
function verifySignature(payload, signature) {
|
|
if (!SECRET) {
|
|
log('WARN: WEBHOOK_SECRET not set — skipping signature check');
|
|
return true;
|
|
}
|
|
const hmac = crypto.createHmac('sha256', SECRET);
|
|
hmac.update(payload);
|
|
const expected = 'sha256=' + hmac.digest('hex');
|
|
try {
|
|
return crypto.timingSafeEqual(
|
|
Buffer.from(expected, 'utf8'),
|
|
Buffer.from(signature, 'utf8')
|
|
);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
if (req.method === 'GET' && req.url === '/health') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ ok: true, deploying }));
|
|
return;
|
|
}
|
|
|
|
if (req.method !== 'POST' || req.url !== '/deploy') {
|
|
res.writeHead(404).end('Not found');
|
|
return;
|
|
}
|
|
|
|
const chunks = [];
|
|
req.on('data', chunk => chunks.push(chunk));
|
|
req.on('end', () => {
|
|
const body = Buffer.concat(chunks);
|
|
const sig = req.headers['x-gitea-signature']
|
|
|| req.headers['x-hub-signature-256']
|
|
|| '';
|
|
|
|
if (!verifySignature(body, sig)) {
|
|
log(`Rejected deploy: invalid HMAC signature from ${req.socket.remoteAddress}`);
|
|
res.writeHead(401).end('Unauthorized');
|
|
return;
|
|
}
|
|
|
|
let payload = {};
|
|
try { payload = JSON.parse(body.toString()); } catch (_) {}
|
|
|
|
const branch = (payload.ref || '').replace('refs/heads/', '');
|
|
if (branch && branch !== 'main') {
|
|
log(`Skipping push to non-main branch: ${branch}`);
|
|
res.writeHead(200).end('Skipped');
|
|
return;
|
|
}
|
|
|
|
if (deploying) {
|
|
log('Deploy already in progress — request queued');
|
|
res.writeHead(202).end('Deploy already in progress');
|
|
return;
|
|
}
|
|
|
|
const pusher = payload.pusher ? payload.pusher.login || payload.pusher.name : 'unknown';
|
|
const commit = payload.after ? payload.after.slice(0, 8) : 'unknown';
|
|
log(`Deploy triggered — pusher: ${pusher}, commit: ${commit}`);
|
|
|
|
res.writeHead(202).end('Deploy started');
|
|
|
|
deploying = true;
|
|
execFile('bash', [DEPLOY_SCRIPT], { timeout: 600_000 }, (err, stdout, stderr) => {
|
|
deploying = false;
|
|
if (err) {
|
|
log(`Deploy FAILED: ${err.message}`);
|
|
} else {
|
|
log(`Deploy succeeded (commit ${commit})`);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, HOST, () => {
|
|
log(`Webhook receiver listening on ${HOST}:${PORT}`);
|
|
log(`Deploy script: ${DEPLOY_SCRIPT}`);
|
|
log(`HMAC secret: ${SECRET ? 'configured' : 'NOT SET (insecure)'}`);
|
|
});
|
|
|
|
process.on('SIGTERM', () => { log('SIGTERM received — shutting down'); process.exit(0); });
|
|
process.on('SIGINT', () => { log('SIGINT received — shutting down'); process.exit(0); });
|