// server.js require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser'); const axios = require('axios'); const app = express(); const { exec, spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); // Create logs directory if it doesn't exist const logsDir = path.join(__dirname, 'logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir); } // Logger function function logMessage(message, type = 'info') { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] [${type.toUpperCase()}] ${message}`; console[type === 'error' ? 'error' : 'log'](logEntry); // Also log to file const logFile = path.join(logsDir, `webhook_${new Date().toISOString().split('T')[0]}.log`); fs.appendFileSync(logFile, logEntry + '\n'); } // Telegram bot function async function sendTelegramMessage(message) { const botToken = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; if (!botToken || !chatId) { logMessage('Telegram bot token or chat ID not configured', 'warn'); return; } try { const url = `https://api.telegram.org/bot${botToken}/sendMessage`; const response = await axios.post(url, { chat_id: chatId, text: message, parse_mode: 'MarkdownV2', disable_web_page_preview: false }); logMessage('Telegram message sent successfully'); return response.data; } catch (error) { logMessage(`Failed to send Telegram message: ${error.message}`, 'error'); if (error.response) { logMessage(`Telegram API error: ${JSON.stringify(error.response.data)}`, 'error'); } } } // Format commit message for Telegram function formatCommitMessage(payload) { const repo = payload.repository?.name || 'Unknown Repository'; const commits = payload.commits || []; const pusher = payload.pusher?.username || payload.pusher?.login || 'Unknown'; if (!commits || commits.length === 0) { return `šŸ”„ *${repo}* - Push event received but no commit details available`; } // Aggregate all files across commits const allAdded = new Set(); const allModified = new Set(); const allRemoved = new Set(); commits.forEach(commit => { (commit.added || []).forEach(file => allAdded.add(file)); (commit.modified || []).forEach(file => allModified.add(file)); (commit.removed || []).forEach(file => allRemoved.add(file)); }); // Format commits list let commitsText = ''; // MarkdownV2 full escaper for arbitrary text (not code) const escapeMdV2 = (s) => { if (s == null) return ''; return String(s) // Order matters: escape backslash first .replace(/\\/g, '\\\\') // Telegram MarkdownV2 reserved characters (in text contexts) .replace(/([_*[\]()~`>#+\-=|{}.!])/g, '\\$1'); }; // Escaper for inline code content inside backticks: only escape backticks and backslash const escapeInlineCode = (s) => { if (s == null) return ''; return String(s) .replace(/\\/g, '\\\\') .replace(/`/g, '\\`'); }; commits.forEach((commit) => { const commitMessage = commit.message?.trim() || 'No commit message'; const escapedMessage = escapeMdV2(commitMessage); const shortHash = commit.id?.substring(0, 7) || 'unknown'; const author = commit.author?.name || 'Unknown'; // backticked hash must use code-escaper rules const hashInline = escapeInlineCode(shortHash); commitsText += `\nšŸ”§ \`${hashInline}\` - ${escapedMessage}`; if (commits.length > 1) { commitsText += ` (${escapeMdV2(author)})`; } }); // Format aggregated file lists let filesList = ''; if (allAdded.size > 0) { filesList += `\nāž• *Added (${allAdded.size}):*\n`; Array.from(allAdded).slice(0, 10).forEach(file => { filesList += ` • \`${escapeInlineCode(file)}\`\n`; }); if (allAdded.size > 10) { filesList += ` • ... and ${allAdded.size - 10} more\n`; } } if (allModified.size > 0) { filesList += `\nšŸ“ *Modified (${allModified.size}):*\n`; Array.from(allModified).slice(0, 10).forEach(file => { filesList += ` • \`${escapeInlineCode(file)}\`\n`; }); if (allModified.size > 10) { filesList += ` • ... and ${allModified.size - 10} more\n`; } } if (allRemoved.size > 0) { filesList += `\nāŒ *Removed (${allRemoved.size}):*\n`; Array.from(allRemoved).slice(0, 10).forEach(file => { filesList += ` • \`${escapeInlineCode(file)}\`\n`; }); if (allRemoved.size > 10) { filesList += ` • ... and ${allRemoved.size - 10} more\n`; } } if (!filesList) { filesList = '\nšŸ“‚ No files changed'; } const totalCommits = commits.length; const commitWord = totalCommits === 1 ? 'commit' : 'commits'; // Escape heading line content and link text/url appropriately const repoEsc = escapeMdV2(repo); const heading = `šŸš€ *${repoEsc}* - ${totalCommits} ${commitWord} pushed`; let compare = ''; if (payload.compare_url) { const linkText = escapeMdV2('Compare Changes'); const linkUrl = escapeMdV2(payload.compare_url); compare = `[${linkText}](${linkUrl})`; } return `${heading} ${commitsText} ${filesList} ${compare}`; } app.use(bodyParser.json()); app.post('/releasehook_kjfhdkf987987', (req, res) => { try { const payload = req.body; logMessage(`Webhook received for repository: ${payload?.repository?.name || 'unknown'}`); // Log the complete payload for analysis logMessage(`Complete payload received: ${JSON.stringify(payload, null, 2)}`); // Send Telegram notification const telegramMessage = formatCommitMessage(payload); sendTelegramMessage(telegramMessage).catch(error => { logMessage(`Error sending Telegram message: ${error.message}`, 'error'); }); // Set a flag to track if we've sent a response let responseSent = false; if(payload && payload.repository && payload.repository.name == 'reactShop'){ // cd /home/seb/src/reactShop ; git pull ; npm run build const repoPath = '/home/seb/src/growheads_de'; const command = `cd ${repoPath} && pkill -f "node.*prerender\\.cjs" || true && git pull && npm i . && npm run build:prerender`; logMessage(`Executing command for reactShop: ${command}`); // Use spawn to stream output line by line // @note Try with explicit options to prevent early termination const child = spawn('bash', ['-c', command], { detached: false, stdio: ['pipe', 'pipe', 'pipe'] }); logMessage(`Started process for reactShop with PID: ${child.pid}`); // Stream stdout line by line child.stdout.on('data', (data) => { const lines = data.toString().split('\n'); lines.forEach(line => { if (line.trim()) { logMessage(`[reactShop stdout] ${line.trim()}`); } }); }); // Stream stderr line by line child.stderr.on('data', (data) => { const lines = data.toString().split('\n'); lines.forEach(line => { if (line.trim()) { logMessage(`[reactShop stderr] ${line.trim()}`, 'warn'); } }); }); // Handle process completion child.on('close', (code, signal) => { if (signal) { logMessage(`Command for reactShop was terminated by signal: ${signal}`, 'error'); if (!responseSent) { responseSent = true; return res.status(500).json({ error: 'Command execution failed', details: `Process terminated by signal: ${signal}` }); } } else if (code !== 0) { logMessage(`Command for reactShop exited with code ${code}`, 'error'); if (!responseSent) { responseSent = true; return res.status(500).json({ error: 'Command execution failed', details: `Process exited with code ${code}` }); } } else { logMessage(`Command for reactShop completed successfully (exit code: ${code})`); if (!responseSent) { responseSent = true; res.status(200).json({ success: true, repository: 'reactShop' }); } } }); // Handle process errors child.on('error', (error) => { logMessage(`Error executing command for reactShop: ${error.message}`, 'error'); if (!responseSent) { responseSent = true; return res.status(500).json({ error: 'Command execution failed', details: error.message }); } }); // Handle process exit (fired before close) child.on('exit', (code, signal) => { if (signal) { logMessage(`Process for reactShop exited due to signal: ${signal}`, 'warn'); } else { logMessage(`Process for reactShop exited with code: ${code}`, code === 0 ? 'info' : 'warn'); } }); return; // Exit early to avoid double response } if(payload && payload.repository && payload.repository.name == 'shopApi'){ // cd /home/seb/src/shopApi ; git pull const repoPath = '/home/seb/src/shopApi'; const command = `cd ${repoPath} && git pull`; logMessage(`Executing command for shopApi: ${command}`); exec(command, (error, stdout, stderr) => { if (error) { logMessage(`Error executing command for shopApi: ${error.message}`, 'error'); if (stderr) { logMessage(`Command stderr: ${stderr}`, 'error'); } if (!responseSent) { responseSent = true; return res.status(500).json({ error: 'Command execution failed', details: error.message }); } } if (stdout) { logMessage(`Command output for shopApi: ${stdout.trim()}`); } if (stderr && !error) { logMessage(`Command stderr (non-fatal) for shopApi: ${stderr}`, 'warn'); } if (!responseSent) { responseSent = true; res.status(200).json({ success: true, repository: 'shopApi' }); } }); return; // Exit early to avoid double response } if(payload && payload.repository && payload.repository.name == 'quickdhl'){ // cd /home/seb/src/shopApi ; git pull const repoPath = '/home/seb/src/quickdhl'; const command = `cd ${repoPath} && git pull`; logMessage(`Executing command for quickdhl: ${command}`); exec(command, (error, stdout, stderr) => { if (error) { logMessage(`Error executing command for quickdhl: ${error.message}`, 'error'); if (stderr) { logMessage(`Command stderr: ${stderr}`, 'error'); } if (!responseSent) { responseSent = true; return res.status(500).json({ error: 'Command execution failed', details: error.message }); } } if (stdout) { logMessage(`Command output for quickdhl: ${stdout.trim()}`); } if (stderr && !error) { logMessage(`Command stderr (non-fatal) for quickdhl: ${stderr}`, 'warn'); } if (!responseSent) { responseSent = true; res.status(200).json({ success: true, repository: 'shopApi' }); } }); return; // Exit early to avoid double response } // If we get here, no repository matched logMessage(`No handler found for repository: ${payload?.repository?.name || 'unknown'}`, 'warn'); res.status(200).json({ success: true, message: 'No action taken' }); } catch (err) { logMessage(`Unhandled exception in webhook handler: ${err.message}`, 'error'); logMessage(err.stack, 'error'); res.status(500).json({ error: 'Internal server error', message: err.message }); } }); // Add a simple health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok' }); }); app.listen(9304, () => logMessage('Webhook server listening on port 9304'));