fix(listener): skip version-bump commits to prevent deploy loop

Parse Gitea webhook payload and skip the deploy when all commits
match 'chore(deploy): bump version'. This breaks the infinite cycle
where the deploy script's own version-bump push re-triggered a deploy,
causing repeated container restarts and 502 errors for users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-05-11 09:42:40 +02:00
parent a54b3f2b19
commit 22891e1274
+42 -23
View File
@@ -10,7 +10,7 @@ const LOG_FILE = '/home/deploy/stack/webhook-listener/deploy.log';
function log(message) { function log(message) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`; const logMessage = '[' + timestamp + '] ' + message + '\n';
console.log(message); console.log(message);
fs.appendFileSync(LOG_FILE, logMessage, 'utf8'); fs.appendFileSync(LOG_FILE, logMessage, 'utf8');
} }
@@ -27,14 +27,14 @@ try {
let isBuilding = false; let isBuilding = false;
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
// Apenas aceita requisições POST na rota /webhook // Apenas aceita requisicoes POST na rota /webhook
const urlPath = req.url.split('?')[0]; const urlPath = req.url.split('?')[0];
if (req.method !== 'POST' || urlPath !== '/webhook') { if (req.method !== 'POST' || urlPath !== '/webhook') {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Rota não encontrada' })); return res.end(JSON.stringify({ error: 'Rota nao encontrada' }));
} }
// Token de validação nos headers (Gitea envia como x-gitea-token) ou query string (?token=...) // Token de validacao nos headers (Gitea envia como x-gitea-token) ou query string (?token=...)
const tokenHeader = req.headers['x-gitea-token']; const tokenHeader = req.headers['x-gitea-token'];
// Parsear query params manualmente de forma simples // Parsear query params manualmente de forma simples
@@ -55,35 +55,54 @@ const server = http.createServer((req, res) => {
const token = tokenHeader || tokenQuery; const token = tokenHeader || tokenQuery;
if (!token || token !== SECRET_TOKEN) { if (!token || token !== SECRET_TOKEN) {
log('⚠️ Tentativa de acesso não autorizada: Token inválido ou ausente.'); log('Tentativa de acesso nao autorizada: Token invalido ou ausente.');
res.writeHead(401, { 'Content-Type': 'application/json' }); res.writeHead(401, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Não autorizado: Token inválido' })); return res.end(JSON.stringify({ error: 'Nao autorizado: Token invalido' }));
} }
if (isBuilding) { if (isBuilding) {
log('Requisição de build recebida, mas já existe um build em andamento. Ignorando para evitar conflito de concorrência.'); log('Requisicao de build recebida, mas ja existe um build em andamento. Ignorando para evitar conflito de concorrencia.');
res.writeHead(429, { 'Content-Type': 'application/json' }); res.writeHead(429, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Um build já está sendo executado neste momento' })); return res.end(JSON.stringify({ error: 'Um build ja esta sendo executado neste momento' }));
} }
// Retorna resposta de confirmação imediata (HTTP 202 Accepted) para evitar timeout do Gitea // Ler o body completo para inspecionar o payload do Gitea
res.writeHead(202, { 'Content-Type': 'application/json' }); let body = '';
res.end(JSON.stringify({ status: 'ok', message: 'Deploy automático iniciado em segundo plano' })); req.on('data', function(chunk) { body += chunk.toString(); });
req.on('end', function() {
// Verificar se todos os commits sao bumps de versao automaticos (evita loop infinito)
var payload = {};
try { payload = JSON.parse(body); } catch (e) {}
// Inicia o processo de deploy var commits = payload.commits || [];
isBuilding = true; var headMsg = (payload.head_commit || {}).message || '';
log('🚀 Iniciando deploy-prod-builder.sh...'); var allMessages = commits.map(function(c) { return c.message || ''; }).concat(headMsg).filter(function(m) { return m.trim() !== ''; });
var VERSION_BUMP_RE = /^chore\(deploy\):\s*bump version/i;
exec(SCRIPT_PATH, (error, stdout, stderr) => { if (allMessages.length > 0 && allMessages.every(function(msg) { return VERSION_BUMP_RE.test(msg); })) {
isBuilding = false; log('Commit de bump de versao detectado -- deploy ignorado para evitar loop de CI.');
if (error) { res.writeHead(200, { 'Content-Type': 'application/json' });
log(`❌ Erro no processo de deploy: ${error.message}`); return res.end(JSON.stringify({ status: 'skipped', reason: 'version-bump commit' }));
return;
} }
log('✅ Processo de deploy finalizado.');
// Retorna resposta de confirmacao imediata (HTTP 202 Accepted) para evitar timeout do Gitea
res.writeHead(202, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', message: 'Deploy automatico iniciado em segundo plano' }));
// Inicia o processo de deploy
isBuilding = true;
log('Iniciando deploy-prod-builder.sh...');
exec(SCRIPT_PATH, function(error, stdout, stderr) {
isBuilding = false;
if (error) {
log('Erro no processo de deploy: ' + error.message);
return;
}
log('Processo de deploy finalizado.');
});
}); });
}); });
server.listen(PORT, HOST, () => { server.listen(PORT, HOST, function() {
log(`📡 Webhook listener ativo em http://${HOST}:${PORT}/webhook`); log('Webhook listener ativo em http://' + HOST + ':' + PORT + '/webhook');
}); });