742 lines
25 KiB
JavaScript
742 lines
25 KiB
JavaScript
// 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');
|
||
|
||
// Use built-in fetch (Node.js 18+) or fallback to node-fetch
|
||
const fetch = globalThis.fetch || require('node-fetch');
|
||
|
||
// AWS Bedrock SDK for Anthropic
|
||
const AnthropicBedrock = require("@anthropic-ai/bedrock-sdk");
|
||
|
||
// --- Helpers: Git log + LLM summarization for Executive Summary (DE) ---
|
||
|
||
// Promise-based exec wrapper with timeout
|
||
function execCmd(cmd, { cwd = process.cwd(), timeoutMs = 8000 } = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const child = require('child_process').exec(cmd, { cwd, maxBuffer: 5 * 1024 * 1024, timeout: timeoutMs }, (error, stdout, stderr) => {
|
||
if (error) {
|
||
return reject(Object.assign(new Error(`exec failed: ${error.message}`), { stdout, stderr }));
|
||
}
|
||
resolve({ stdout, stderr });
|
||
});
|
||
// Best-effort: if process times out, Node will error; no extra handling here
|
||
});
|
||
}
|
||
|
||
// Retrieve and parse last N git commits (subject + body)
|
||
async function getLastCommits({ count = 10, repoDir = process.cwd() } = {}) {
|
||
try {
|
||
const format = '%H%x1f%an%x1f%ad%x1f%s%x1f%b';
|
||
const cmd = `git log -n ${count} --pretty=format:${format} --date=iso`;
|
||
const { stdout } = await execCmd(cmd, { cwd: repoDir, timeoutMs: 8000 });
|
||
const lines = stdout.split('\n').filter(Boolean);
|
||
return lines.map(line => {
|
||
const [hash, author, date, subject, body = ''] = line.split('\x1f');
|
||
return { hash, author, date, subject, body };
|
||
});
|
||
} catch (e) {
|
||
logMessage(`getLastCommits failed: ${e.message}`, 'warn');
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Get unified diff for the latest commit (against its parent)
|
||
async function getLastCommitDiff({ repoDir = process.cwd(), contextLines = 3 } = {}) {
|
||
try {
|
||
// --pretty=format: to avoid commit message in diff header; use -1 for last commit
|
||
const cmd = `git show -1 --unified=${contextLines} --no-color`;
|
||
const { stdout } = await execCmd(cmd, { cwd: repoDir, timeoutMs: 8000 });
|
||
return stdout || '';
|
||
} catch (e) {
|
||
logMessage(`getLastCommitDiff failed: ${e.message}`, 'warn');
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function buildMostRecentCommitPromptGerman(commits) {
|
||
if (!commits || commits.length === 0) return null;
|
||
const c0 = commits[0];
|
||
// Keep prompt compact but informative
|
||
const latest = [
|
||
`Commit: ${c0.hash}`,
|
||
`Autor: ${c0.author}`,
|
||
`Datum: ${c0.date}`,
|
||
`Betreff: ${c0.subject}`,
|
||
`Inhalt:\n${c0.body || '(kein Body)'}`
|
||
].join('\n');
|
||
|
||
const history = commits.slice(0, 10).map(c => `- ${c.hash.slice(0,7)} | ${c.subject}`).join('\n');
|
||
|
||
return {
|
||
system: 'Du bist ein erfahrener Software-Produktmanager. Erstelle eine kurze, laienverständliche, executive-taugliche Zusammenfassung der Auswirkungen der jüngsten Änderung. Vermeide Fachjargon, nenne das „Warum“ und den Nutzen. Antworte ausschließlich auf Deutsch in 1–3 Sätzen. Erkläre nicht die Vorteile für Führungskräfte, erkläre nur was sich am Code geändert hat.',
|
||
user: `Hier ist der Git-Verlauf (letzte 10 Commits); fokussiere die jüngste Änderung:\n\nVerlauf:\n${history}\n\nDetail der neuesten Änderung:\n${latest}\n\nGib eine kurze Executive-Zusammenfassung (Deutsch, 1–3 Sätze).`
|
||
};
|
||
}
|
||
|
||
// Generic OpenAI-compatible client over fetch
|
||
async function callLLMOpenAICompatible({ baseUrl, apiKey, model, system, user, timeoutMs = 12000 }) {
|
||
const controller = new AbortController();
|
||
const t = setTimeout(() => controller.abort(), timeoutMs);
|
||
try {
|
||
const res = await fetch(`${baseUrl}/v1/chat/completions`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${apiKey}`
|
||
},
|
||
body: JSON.stringify({
|
||
model,
|
||
messages: [
|
||
{ role: 'system', content: system },
|
||
{ role: 'user', content: user }
|
||
],
|
||
temperature: 0.2
|
||
}),
|
||
signal: controller.signal
|
||
});
|
||
if (!res.ok) {
|
||
const text = await res.text().catch(() => '');
|
||
throw new Error(`LLM HTTP ${res.status}: ${text.slice(0, 500)}`);
|
||
}
|
||
const json = await res.json();
|
||
const msg = json.choices?.[0]?.message?.content?.trim();
|
||
return msg || '';
|
||
} finally {
|
||
clearTimeout(t);
|
||
}
|
||
}
|
||
|
||
// AWS Bedrock client for Anthropic
|
||
async function callLLMBedrock({ awsAccessKey, awsSecretKey, awsRegion, model, system, user }) {
|
||
try {
|
||
const abclient = new AnthropicBedrock({
|
||
awsAccessKey,
|
||
awsSecretKey,
|
||
awsRegion
|
||
});
|
||
|
||
// Bedrock API uses a different format - system is a separate parameter
|
||
const stream = await abclient.messages.create({
|
||
model,
|
||
system: system,
|
||
stream: true,
|
||
messages: [
|
||
{ role: 'user', content: user }
|
||
],
|
||
max_tokens: 64000,
|
||
});
|
||
|
||
let result = "";
|
||
|
||
for await (const messageStreamEvent of stream) {
|
||
if (messageStreamEvent.type === "content_block_delta") {
|
||
if (messageStreamEvent.delta.type === "text_delta") {
|
||
result += messageStreamEvent.delta.text;
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
} catch (e) {
|
||
throw new Error(`Bedrock API error: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// Get commit diff from Gitea API
|
||
async function getCommitDiffFromGitea(giteaBaseUrl, repoOwner, repoName, commitHash) {
|
||
const giteaToken = process.env.GITEA_TOKEN;
|
||
if (!giteaToken) {
|
||
logMessage('GITEA_TOKEN not configured; cannot fetch diff from API', 'warn');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const apiUrl = `${giteaBaseUrl}/api/v1/repos/${repoOwner}/${repoName}/git/commits/${commitHash}.diff`;
|
||
logMessage(`Fetching commit diff from: ${apiUrl}`);
|
||
|
||
const response = await fetch(apiUrl, {
|
||
headers: {
|
||
'Authorization': `token ${giteaToken}`,
|
||
'Accept': 'text/plain'
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
if (!response.ok) {
|
||
logMessage(`Gitea API request failed: ${response.status} ${response.statusText}`, 'warn');
|
||
return null;
|
||
}
|
||
|
||
const diffText = await response.text();
|
||
|
||
if (diffText && diffText.trim()) {
|
||
return diffText;
|
||
}
|
||
|
||
return null;
|
||
} catch (e) {
|
||
logMessage(`Failed to fetch commit diff from Gitea API: ${e.message}`, 'warn');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Summarize the most recent commit in German using webhook payload data
|
||
async function summarizeMostRecentCommitDE(webhookPayload = null) {
|
||
// Determine provider
|
||
const provider = (process.env.LLM_PROVIDER || 'openrouter').toLowerCase();
|
||
const model = process.env.LLM_MODEL || (provider === 'openrouter' ? 'openrouter/anthropic/claude-3.5-sonnet' : 'gpt-4o-mini');
|
||
let baseUrl;
|
||
let apiKey;
|
||
let awsAccessKey;
|
||
let awsSecretKey;
|
||
let awsRegion;
|
||
|
||
if (provider === 'bedrock') {
|
||
// AWS Bedrock configuration
|
||
awsAccessKey = process.env.AWS_ACCESS_KEY;
|
||
awsSecretKey = process.env.AWS_SECRET_KEY;
|
||
awsRegion = process.env.AWS_REGION || 'eu-central-1';
|
||
|
||
if (!awsAccessKey || !awsSecretKey) {
|
||
logMessage('AWS credentials not configured; skipping executive summary', 'warn');
|
||
return null;
|
||
}
|
||
} else if (provider === 'openai') {
|
||
baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com';
|
||
apiKey = process.env.OPENAI_API_KEY;
|
||
|
||
if (!apiKey) {
|
||
logMessage('LLM API key not configured; skipping executive summary', 'warn');
|
||
return null;
|
||
}
|
||
} else {
|
||
// default openrouter
|
||
baseUrl = process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api';
|
||
apiKey = process.env.OPENROUTER_API_KEY;
|
||
|
||
if (!apiKey) {
|
||
logMessage('LLM API key not configured; skipping executive summary', 'warn');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Require webhook payload - no fallback to local git
|
||
if (!webhookPayload || !webhookPayload.commits || webhookPayload.commits.length === 0) {
|
||
logMessage('No webhook payload or commits provided for executive summary', 'warn');
|
||
return null;
|
||
}
|
||
|
||
// Use webhook payload data
|
||
const commits = webhookPayload.commits.map(commit => ({
|
||
hash: commit.id,
|
||
author: commit.author?.name || 'Unknown',
|
||
date: commit.timestamp || new Date().toISOString(),
|
||
subject: commit.message?.split('\n')[0] || 'No subject',
|
||
body: commit.message?.split('\n').slice(1).join('\n') || ''
|
||
}));
|
||
|
||
// Extract Gitea base URL from webhook payload
|
||
const giteaBaseUrl = webhookPayload.repository?.html_url?.match(/^https?:\/\/[^\/]+/)?.[0];
|
||
const repoOwner = webhookPayload.repository?.owner?.login;
|
||
const repoName = webhookPayload.repository?.name;
|
||
const latestCommitHash = commits[0]?.hash;
|
||
|
||
if (!giteaBaseUrl || !repoOwner || !repoName || !latestCommitHash) {
|
||
logMessage('Missing required repository information in webhook payload for executive summary', 'warn');
|
||
return null;
|
||
}
|
||
|
||
const diff = await getCommitDiffFromGitea(giteaBaseUrl, repoOwner, repoName, latestCommitHash);
|
||
|
||
const prompt = buildMostRecentCommitPromptGerman(commits);
|
||
if (!prompt) return null;
|
||
|
||
// Include the diff in the system prompt
|
||
const systemWithDiff =
|
||
`${prompt.system}\n\n` +
|
||
`Kontext (unified diff der neuesten Änderung):\n` +
|
||
`${diff ? diff.slice(0, 120000) : '(kein Diff verfügbar)'}`;
|
||
|
||
try {
|
||
let summary;
|
||
|
||
if (provider === 'bedrock') {
|
||
|
||
summary = await callLLMBedrock({
|
||
awsAccessKey,
|
||
awsSecretKey,
|
||
awsRegion,
|
||
model,
|
||
system: systemWithDiff,
|
||
user: prompt.user
|
||
});
|
||
} else {
|
||
// Use OpenAI-compatible API (openai, openrouter, etc.)
|
||
summary = await callLLMOpenAICompatible({
|
||
baseUrl,
|
||
apiKey,
|
||
model,
|
||
system: systemWithDiff,
|
||
user: prompt.user,
|
||
timeoutMs: 20000
|
||
});
|
||
}
|
||
|
||
if (!summary) return null;
|
||
return summary;
|
||
} catch (e) {
|
||
logMessage(`LLM summarization failed: ${e.message}`, 'warn');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Create logs directory if it doesn't exist
|
||
const logsDir = path.join(__dirname, 'logs');
|
||
if (!fs.existsSync(logsDir)) {
|
||
fs.mkdirSync(logsDir);
|
||
}
|
||
|
||
// Create failed webhooks directory if it doesn't exist
|
||
const failedWebhooksDir = path.join(__dirname, 'failed_webhooks');
|
||
if (!fs.existsSync(failedWebhooksDir)) {
|
||
fs.mkdirSync(failedWebhooksDir);
|
||
}
|
||
|
||
// 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');
|
||
}
|
||
|
||
// Function to dump failed webhooks for retry
|
||
function dumpFailedWebhook(payload, error, reason = 'unknown') {
|
||
try {
|
||
const timestamp = new Date().toISOString();
|
||
const filename = `failed_webhook_${timestamp.replace(/[:.]/g, '-')}_${payload?.repository?.name || 'unknown'}.json`;
|
||
const filepath = path.join(failedWebhooksDir, filename);
|
||
|
||
const failedWebhookData = {
|
||
timestamp,
|
||
reason,
|
||
error: error?.message || String(error),
|
||
payload
|
||
};
|
||
|
||
fs.writeFileSync(filepath, JSON.stringify(failedWebhookData, null, 2));
|
||
logMessage(`Failed webhook dumped to: ${filename}`, 'info');
|
||
return filepath;
|
||
} catch (dumpError) {
|
||
logMessage(`Failed to dump webhook: ${dumpError.message}`, 'error');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 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`;
|
||
logMessage(`Sending Telegram message: ${message}`, 'info');
|
||
|
||
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');
|
||
logMessage(`Telegram API status: ${error.response.status}`, 'error');
|
||
}
|
||
|
||
// Create a more detailed error for webhook failure logging
|
||
const detailedError = new Error(`Telegram API error: ${error.message}`);
|
||
if (error.response && error.response.data) {
|
||
detailedError.message = `Telegram API error (${error.response.status}): ${JSON.stringify(error.response.data)}`;
|
||
}
|
||
|
||
// Re-throw the detailed error so it can be caught by the calling code
|
||
throw detailedError;
|
||
}
|
||
}
|
||
|
||
// 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: escape backticks, backslash, and periods
|
||
const escapeInlineCode = (s) => {
|
||
if (s == null) return '';
|
||
return String(s)
|
||
.replace(/\\/g, '\\\\')
|
||
.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);
|
||
|
||
// Replace raw hyphen with EN DASH between hash and message to avoid '-' entity errors
|
||
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 += ` • ${escapeMdV2(`... 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 += ` • ${escapeMdV2(`... 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 += ` • ${escapeMdV2(`... 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);
|
||
// Use en dash and keep the rest escaped via escapeMdV2 for safety
|
||
const heading = `🚀 *${repoEsc}* – ${totalCommits} ${commitWord} pushed`;
|
||
|
||
let compare = '';
|
||
if (payload.compare_url) {
|
||
// Safer to avoid Markdown link syntax; print escaped text and URL
|
||
const linkText = escapeMdV2('Compare Changes');
|
||
const linkUrl = escapeMdV2(payload.compare_url);
|
||
compare = `${linkText}: ${linkUrl}`;
|
||
}
|
||
|
||
// Try to append a German executive summary of the most recent commit.
|
||
// Reuse escapeMdV2 defined above.
|
||
|
||
return (async () => {
|
||
let summaryBlock = '';
|
||
try {
|
||
const summary = await summarizeMostRecentCommitDE(payload);
|
||
if (summary) {
|
||
const escapedSummary = escapeMdV2(summary);
|
||
summaryBlock = `\n\n———————————————\n\n📋 *Executive Summary*\n\n${escapedSummary}\n\n\n`;
|
||
}
|
||
} catch (e) {
|
||
// already logged inside summarizer; keep silent here
|
||
logMessage(`Executive summary generation failed: ${e.message}`, 'warn');
|
||
}
|
||
|
||
// Ensure each section starts on its own line; compare may be plain URL text.
|
||
return `${heading}
|
||
${commitsText}
|
||
${filesList}
|
||
${compare ? `\n${compare}` : ''}${summaryBlock}`;
|
||
})();
|
||
}
|
||
|
||
app.use(bodyParser.json());
|
||
|
||
app.post('/releasehook_kjfhdkf987987', async (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 synchronously so we can return the error
|
||
try {
|
||
const telegramMessage = await Promise.resolve(formatCommitMessage(payload));
|
||
await sendTelegramMessage(telegramMessage);
|
||
logMessage('Telegram message sent successfully');
|
||
} catch (error) {
|
||
logMessage(`Error sending Telegram message: ${error.message}`, 'error');
|
||
dumpFailedWebhook(payload, error, 'telegram_send_error');
|
||
// Return error response instead of 200
|
||
return res.status(400).json({
|
||
error: 'Telegram send failed',
|
||
details: error.message
|
||
});
|
||
}
|
||
|
||
// 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(payload && payload.repository && payload.repository.name == 'dashApp'){
|
||
// Check if push was to 'prod' branch
|
||
const targetBranch = 'prod';
|
||
const ref = payload.ref || '';
|
||
const branchName = ref.replace('refs/heads/', '');
|
||
|
||
if (!responseSent) {
|
||
responseSent = true;
|
||
res.status(200).json({ success: true, repository: 'dashApp', message: 'No action taken - not prod branch' });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 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'));
|