Files
deployHook/x.js
2025-08-04 10:19:29 +02:00

369 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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'));