// Webhook receiver for Gitea push events // This service listens for webhook events from Gitea and triggers a deployment // when changes are pushed to the monitored branches const express = require('express'); const http = require('http'); const crypto = require('crypto'); const { exec } = require('child_process'); const path = require('path'); const fs = require('fs'); // Configuration (can be moved to environment variables) const config = { port: process.env.WEBHOOK_PORT || 9000, secret: process.env.WEBHOOK_SECRET || 'change-this-secret-in-production', deployScript: process.env.DEPLOY_SCRIPT || path.join(__dirname, 'webhook-deploy.sh'), logFile: process.env.LOG_FILE || path.join(__dirname, '../logs/webhook.log'), allowedBranches: (process.env.ALLOWED_BRANCHES || 'main,master').split(','), allowedRepositories: (process.env.ALLOWED_REPOSITORIES || 'mwhitnessing').split(',') }; // Create logs directory if it doesn't exist const logsDir = path.dirname(config.logFile); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } // Create Express app const app = express(); app.use(express.json()); // Helper function to log messages function log(message) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; console.log(logMessage.trim()); fs.appendFileSync(config.logFile, logMessage); } // Verify Gitea webhook signature function verifySignature(req) { const signature = req.headers['x-gitea-signature']; if (!signature) { return false; } const hmac = crypto.createHmac('sha256', config.secret); const computedSignature = hmac.update(JSON.stringify(req.body)).digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(computedSignature) ); } // Handle Gitea push webhook app.post('/webhook', (req, res) => { // Log receipt of webhook log('Received webhook request'); try { // Check if webhook signature is valid if (!verifySignature(req)) { log('Invalid webhook signature'); return res.status(403).send('Invalid signature'); } // Extract relevant information from the webhook payload const { ref, repository } = req.body; // Check if this is a branch we care about const branchName = ref.replace('refs/heads/', ''); if (!config.allowedBranches.includes(branchName)) { log(`Ignoring push to branch: ${branchName}`); return res.status(200).send('Ignored branch'); } // Check if this is a repository we care about const repoName = repository.name; if (!config.allowedRepositories.includes(repoName)) { log(`Ignoring push to repository: ${repoName}`); return res.status(200).send('Ignored repository'); } // Log the event log(`Received push event for ${repoName}/${branchName}`); // Respond to webhook immediately res.status(200).send('Processing deployment'); // Execute the deployment script with the branch name const command = `${config.deployScript} ${branchName}`; log(`Executing: ${command}`); exec(command, (error, stdout, stderr) => { if (error) { log(`Deployment error: ${error.message}`); return; } if (stderr) { log(`Deployment stderr: ${stderr}`); } log(`Deployment stdout: ${stdout}`); log('Deployment completed successfully'); }); } catch (error) { log(`Error processing webhook: ${error.message}`); if (!res.headersSent) { res.status(500).send('Error processing webhook'); } } }); // Start the server const server = http.createServer(app); server.listen(config.port, () => { log(`Webhook receiver listening on port ${config.port}`); log(`Monitoring branches: ${config.allowedBranches.join(', ')}`); log(`Monitoring repositories: ${config.allowedRepositories.join(', ')}`); }); // Handle server shutdown process.on('SIGTERM', () => { log('Shutting down webhook receiver...'); server.close(() => { log('Webhook receiver stopped'); process.exit(0); }); }); process.on('SIGINT', () => { log('Shutting down webhook receiver...'); server.close(() => { log('Webhook receiver stopped'); process.exit(0); }); });