From 00fcbc9a7caf985509e0e0a11414dcca9c490fe8 Mon Sep 17 00:00:00 2001 From: javiservices Date: Thu, 18 Jun 2026 12:16:48 +0200 Subject: [PATCH] fix(deploy): excluir CSRF en /webhook/* + script self-contained - bootstrap/app.php: validateCsrfTokens except webhook/* - WebhookController: ejecuta deploy-runner.sh (vive en repo) - deploy-runner.sh: hace git pull + composer + npm + migrate + cache - docker-compose.yml: solo monta el repo (sin script externo) --- app/Http/Controllers/WebhookController.php | 54 +++++++++++++------- bootstrap/app.php | 5 ++ deploy-runner.sh | 59 ++++++++++++++++++++++ docker-compose.yml | 2 - 4 files changed, 99 insertions(+), 21 deletions(-) create mode 100755 deploy-runner.sh diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index 88bf68a..aaa6213 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -5,15 +5,21 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Symfony\Component\Process\Process; class WebhookController extends Controller { /** - * Webhook de Gitea para disparar el deploy. - * Gitea envía POST con X-Hub-Signature-256 (HMAC-SHA256). - * Si la firma coincide, lanza el script de deploy en background. + * Webhook de Gitea → deploy self-contained. + * + * Gitea envía POST al hacer push a main, con X-Hub-Signature-256. + * Si la firma coincide, lanza `git pull` + migrate + cache clear + * en background usando el binario git del container. + * + * IMPORTANTE: este código debe existir ANTES de que el webhook + * se active. El primer deploy se hace a mano vía SSH. */ public function deploy(Request $r): JsonResponse { - $secret = config('app.deploy_webhook_secret', env('DEPLOY_WEBHOOK_SECRET')); + $secret = env('DEPLOY_WEBHOOK_SECRET'); $body = $r->getContent(); $sig = $r->header('X-Hub-Signature-256', ''); @@ -26,27 +32,37 @@ public function deploy(Request $r): JsonResponse { $ref = $payload['ref'] ?? ''; $branch = str_starts_with($ref, 'refs/heads/') ? substr($ref, 10) : 'main'; - // Solo main (o el branch que se quiera) if (! in_array($branch, ['main', 'master'], true)) { Log::info("[webhook] push a '$branch' ignorado"); return response()->json(['ok' => true, 'skipped' => 'branch != main'], 200); } - // Lanza el deploy en background, desacoplado del request - $script = '/usr/local/bin/dummybot-deploy.sh'; - if (is_executable($script)) { - $logDir = '/var/log/dummybot-deploy'; - @mkdir($logDir, 0755, true); - $logFile = $logDir . '/deploy-' . date('Ymd-His') . '.log'; - $cmd = sprintf('BRANCH=%s nohup %s >> %s 2>&1 &', escapeshellarg($branch), $script, escapeshellarg($logFile)); - exec($cmd); - Log::info("[webhook] deploy branch=$branch lanzado log=$logFile"); - return response()->json(['ok' => true, 'status' => 'deploy started', 'log' => basename($logFile)], 202); - } + $logFile = storage_path('logs/deploy-' . date('Ymd-His') . '.log'); + $this->dispatchDeployInBackground($branch, $logFile); - // Si el script no está (entorno local / sin permisos) — solo log - Log::info("[webhook] push branch=$branch (script no disponible)"); - return response()->json(['ok' => true, 'status' => 'logged only'], 200); + Log::info("[webhook] deploy branch=$branch lanzado log=$logFile"); + return response()->json([ + 'ok' => true, + 'status' => 'deploy started', + 'log' => basename($logFile), + ], 202); + } + + private function dispatchDeployInBackground(string $branch, string $logFile): void { + $script = base_path('deploy-runner.sh'); + if (! file_exists($script)) { + // Modo fallback: registrar en log y salir + Log::warning("[webhook] $script no existe, deploy no ejecutado"); + return; + } + $cmd = sprintf( + 'BRANCH=%s BRANCH=%s nohup bash %s > %s 2>&1 &', + escapeshellarg($branch), + escapeshellarg($branch), + escapeshellarg($script), + escapeshellarg($logFile) + ); + exec($cmd); } private function validSignature(string $body, string $sig, string $secret): bool { diff --git a/bootstrap/app.php b/bootstrap/app.php index c5e9963..14f0b8a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -16,6 +16,11 @@ $middleware->alias([ 'auth.device' => \App\Http\Middleware\AuthenticateDevice::class, ]); + + // Excluir CSRF para el webhook de Gitea (autenticado por HMAC) + $middleware->validateCsrfTokens(except: [ + 'webhook/*', + ]); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/deploy-runner.sh b/deploy-runner.sh new file mode 100755 index 0000000..6b752ca --- /dev/null +++ b/deploy-runner.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# DummyBot – deploy runner (vive en el repo) +# Llamado por el webhook de Gitea. Hace git pull + composer + +# npm + migrate + cache clear + restart. +# +# IMPORTANTE: este script se ejecuta DENTRO del container +# dummybot_app (PHP-FPM). Por tanto: +# - git, composer, npm están disponibles (Dockerfile los instala) +# - artisan funciona con `php artisan` +# - `docker compose` se puede invocar contra el socket del host +# ───────────────────────────────────────────────────────────── +set -e +BRANCH="${BRANCH:-main}" +APP_DIR="${APP_DIR:-/var/www}" +cd "$APP_DIR" + +echo "═══════════════════════════════════════════" +echo " DummyBot deploy — $(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo " Branch: $BRANCH" +echo " App dir: $APP_DIR" +echo "═══════════════════════════════════════════" + +echo "→ git fetch + reset" +git fetch origin "$BRANCH" 2>&1 | tail -3 +git reset --hard "origin/$BRANCH" 2>&1 | tail -3 +NEW_COMMIT=$(git rev-parse --short HEAD) +echo "HEAD: $NEW_COMMIT" + +echo "→ composer install" +composer install --no-dev --no-interaction --no-progress --optimize-autoloader 2>&1 | tail -3 + +echo "→ npm install + build" +if [ -f package-lock.json ]; then + npm ci --no-audit --no-fund 2>&1 | tail -3 +else + npm install --no-audit --no-fund 2>&1 | tail -3 +fi +npm run build 2>&1 | tail -3 || echo " (build skipped)" + +echo "→ artisan migrate" +php artisan migrate --force 2>&1 | tail -5 + +echo "→ artisan cache:clear" +php artisan config:clear 2>&1 | tail -1 +php artisan route:clear 2>&1 | tail -1 +php artisan view:clear 2>&1 | tail -1 +php artisan event:clear 2>&1 | tail -1 +php artisan optimize 2>&1 | tail -3 + +echo "→ restart php-fpm (graceful)" +# Recargar el FPM para que coja el nuevo código +if command -v kill &> /dev/null; then + kill -USR2 1 2>&1 || true +fi + +echo "═══════════════════════════════════════════" +echo " Deploy OK: $NEW_COMMIT" +echo "═══════════════════════════════════════════" diff --git a/docker-compose.yml b/docker-compose.yml index 26ebc5b..e0ae933 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,6 @@ services: working_dir: /var/www volumes: - .:/var/www - - /usr/local/bin/dummybot-deploy.sh:/usr/local/bin/dummybot-deploy.sh:ro - - /var/log/dummybot-deploy:/var/log/dummybot-deploy environment: - AUTO_MIGRATE=${AUTO_MIGRATE:-0} - DEPLOY_WEBHOOK_SECRET=${DEPLOY_WEBHOOK_SECRET:-}