From e9244c0de5aafdc5b9a92ee71271570bdd68f259 Mon Sep 17 00:00:00 2001 From: davidpp09 Date: Tue, 28 Jul 2026 10:44:03 -0600 Subject: [PATCH] feat: despliegue versionado del frontend con rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hasta ahora Caddy servia /home/david/RestFoodF/dist, la misma carpeta que escribe npm run build: compilar y publicar eran el mismo acto. Un build de prueba tiraba a produccion lo que hubiera en el arbol de trabajo, y no habia version anterior a la que volver porque el build ya la habia sobrescrito. Costo dos incidentes el 2026-07-27. Ahora Caddy sirve ~/deploys/restfood-frontend/current, un symlink a una release con fecha y sha. Publicar es mover el symlink -- atomico y sin recargar Caddy. deploy-frontend.sh exige main limpio y el check 'tests' en verde, aborta si existe .env.local (Vite lo carga tambien en producción y asi fue como el frontend acabo apuntando a staging), verifica que el bundle que pide index.html exista, comprueba contra el sitio real que se este sirviendo ese bundle y revierte solo si no. rollback-frontend.sh intercambia current y previous. Aqui el rollback siempre funciona: son archivos estaticos, no hay migraciones de por medio como en el backend. --- ARCHITECTURE.md | 16 +++++- CLAUDE.md | 11 ++++- deploy/deploy-frontend.sh | 99 +++++++++++++++++++++++++++++++++++++ deploy/rollback-frontend.sh | 32 ++++++++++++ 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100755 deploy/deploy-frontend.sh create mode 100755 deploy/rollback-frontend.sh diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 098be0b..cb688c6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -35,10 +35,22 @@ Cada capa tiene un solo trabajo: ## Deploy ```bash -npm run build # genera dist/, Caddy lo sirve directo — no hay que reiniciar nada +./deploy/deploy-frontend.sh # publica en producción +./deploy/rollback-frontend.sh # vuelve a la release anterior ``` -La tablet cachea fuerte: después de un build, pull-to-refresh en Fully Kiosk para ver el cambio. +`npm run build` **solo compila**: escribe en el `dist/` del repo, que no lo sirve +nadie. Publicar es otra cosa — el script copia el build a una release con fecha y +sha en `~/deploys/restfood-frontend/releases/` y mueve el symlink `current`, que +es lo que Caddy sirve. Mover un symlink es atómico y no hay que recargar Caddy. + +Hasta el 2026-07-28 Caddy servía el `dist/` del repo directo, así que compilar y +publicar eran el mismo acto: probar un build tiraba a producción lo que hubiera en +el árbol de trabajo, y no había versión anterior a la que volver porque el build +la había sobrescrito. + +La tablet cachea fuerte: después de un deploy, pull-to-refresh en Fully Kiosk para +ver el cambio (el `index.html` se sirve con `no-cache`, así que basta una recarga). ## Notas diff --git a/CLAUDE.md b/CLAUDE.md index a338522..8f3644e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,9 +11,18 @@ Guía para Claude Code al trabajar en este repo. Contexto general en `README.md` ```bash npm run dev # desarrollo npm run lint # linter — SIEMPRE antes de abrir PR -npm run build # build de producción (dist/) +npm run build # compila a dist/ — NO publica nada, es carpeta de trabajo ``` +## Deploy +```bash +./deploy/deploy-frontend.sh # main + CI verde -> release versionada + symlink current +./deploy/rollback-frontend.sh # vuelve a la release anterior +``` +Caddy sirve `~/deploys/restfood-frontend/current`, no el `dist/` del repo. Antes +de 2026-07-28 sí servía el `dist/`, y por eso un `npm run build` de prueba +publicaba en producción. + ## Flujo de trabajo (obligatorio) 1. Nunca commitear directo a `main`. 2. Crear rama: `feat/` o `fix/`. diff --git a/deploy/deploy-frontend.sh b/deploy/deploy-frontend.sh new file mode 100755 index 0000000..6f3eac7 --- /dev/null +++ b/deploy/deploy-frontend.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Deploy del frontend RestFood — el equivalente del deploy/deploy.sh del backend. +# +# POR QUÉ EXISTE: +# +# Hasta el 2026-07-28 no había despliegue del frontend: `npm run build` escribía +# directo en /home/david/RestFoodF/dist, que era EXACTAMENTE la carpeta que Caddy +# servía en producción. Compilar y publicar eran el mismo acto, así que: +# - probar un build tiraba a producción lo que hubiera en el árbol de trabajo, +# - no había forma de volver a la versión anterior: la anterior ya no existía, +# - un build a medias dejaba el dist a medias, en vivo. +# Costó dos incidentes el 2026-07-27 (uno de ellos publicó un frontend apuntando +# a staging, porque Vite carga .env.local también en builds de producción). +# +# Ahora Caddy sirve /home/david/deploys/restfood-frontend/current, un symlink a +# una release con fecha y sha. Publicar es mover el symlink: es atómico, y el +# `dist/` del repo vuelve a ser lo que siempre debió ser — una carpeta de trabajo +# que no le importa a nadie. +# +# Uso: ./deploy/deploy-frontend.sh +set -euo pipefail + +REPO_DIR="$HOME/RestFoodF" +DEPLOY_DIR="$HOME/deploys/restfood-frontend" +RELEASES_DIR="$DEPLOY_DIR/releases" +URL_VERIFICACION="https://192.168.10.100/" +KEEP_RELEASES=5 + +log() { echo "[deploy-front] $*"; } +die() { echo "[deploy-front] ERROR: $*" >&2; exit 1; } + +# 1. Solo se despliega main, limpio y actualizado +cd "$REPO_DIR" +[[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]] || die "hay que estar en main (estás en $(git rev-parse --abbrev-ref HEAD))" +[[ -z "$(git status --porcelain)" ]] || die "hay cambios sin commitear; el deploy solo usa código versionado" +git pull --ff-only +SHA="$(git rev-parse --short HEAD)" + +# 2. Solo se despliega lo que el CI ya aprobó +ESTADO_CI="$(gh api "repos/davidpp09/RestFoodF/commits/$SHA/check-runs" \ + --jq '[.check_runs[] | select(.name=="tests")][0].conclusion' 2>/dev/null || echo desconocido)" +[[ "$ESTADO_CI" == "success" ]] || die "el check 'tests' del commit $SHA no está en verde (estado: $ESTADO_CI)" + +# 3. Construir +# +# Ojo con .env.local: Vite lo carga TAMBIÉN en producción (solo se salta los +# .env.*.local del modo contrario). Un archivo olvidado apuntando a staging +# viaja dentro del bundle sin que nada avise. Por eso se aborta si existe. +[[ -e "$REPO_DIR/.env.local" ]] && die ".env.local existe y Vite lo carga en producción — renómbralo antes de desplegar" + +log "construyendo $SHA (build limpio)..." +rm -rf "$REPO_DIR/dist" +npm ci --silent +npm run build + +[[ -f "$REPO_DIR/dist/index.html" ]] || die "el build no generó dist/index.html" + +# El index.html referencia el bundle por nombre con hash. Si ese archivo no está +# en el dist, la tablet carga un HTML que pide un JS inexistente: pantalla en +# blanco sin un solo error en el servidor. +BUNDLE="$(grep -oE 'assets/index-[A-Za-z0-9_-]+\.js' "$REPO_DIR/dist/index.html" | head -1)" +[[ -n "$BUNDLE" ]] || die "no se encontró la referencia al bundle en index.html" +[[ -f "$REPO_DIR/dist/$BUNDLE" ]] || die "index.html pide $BUNDLE y ese archivo no está en dist/" +log "bundle del build: $BUNDLE" + +# 4. Guardar la release y apuntar current (previous queda para el rollback) +mkdir -p "$RELEASES_DIR" +RELEASE="$RELEASES_DIR/$(date +%Y%m%d-%H%M%S)-$SHA" +cp -r "$REPO_DIR/dist" "$RELEASE" + +if [[ -e "$DEPLOY_DIR/current" ]]; then + ln -sfn "$(readlink -f "$DEPLOY_DIR/current")" "$DEPLOY_DIR/previous" +fi +# `ln -sfn` sobre el symlink es lo más cerca de atómico que hay aquí: ninguna +# petición ve un estado intermedio, y no hace falta recargar Caddy — la ruta se +# resuelve en cada petición. +ln -sfn "$RELEASE" "$DEPLOY_DIR/current" + +# 5. Verificar contra el servidor real, no contra el disco +CODIGO="$(curl -s -k -o /dev/null -w '%{http_code}' -m 10 "$URL_VERIFICACION" || true)" +SERVIDO="$(curl -s -k -m 10 "$URL_VERIFICACION" | grep -oE 'assets/index-[A-Za-z0-9_-]+\.js' | head -1 || true)" +if [[ "$CODIGO" != "200" || "$SERVIDO" != "$BUNDLE" ]]; then + log "la verificación falló (HTTP $CODIGO, sirviendo '$SERVIDO', se esperaba '$BUNDLE') — ROLLBACK automático" + if [[ -e "$DEPLOY_DIR/previous" ]]; then + ln -sfn "$(readlink -f "$DEPLOY_DIR/previous")" "$DEPLOY_DIR/current" + fi + die "deploy fallido; se restauró la versión anterior" +fi + +# 6. Conservar solo las últimas $KEEP_RELEASES (nunca la actual ni la anterior) +ls -dt "$RELEASES_DIR"/*/ 2>/dev/null | tail -n +$((KEEP_RELEASES + 1)) | while read -r vieja; do + vieja="${vieja%/}" + [[ "$vieja" == "$(readlink -f "$DEPLOY_DIR/current")" ]] && continue + [[ -e "$DEPLOY_DIR/previous" && "$vieja" == "$(readlink -f "$DEPLOY_DIR/previous")" ]] && continue + rm -rf "$vieja" +done + +log "desplegado $SHA → $(basename "$RELEASE") (HTTP $CODIGO, sirviendo $BUNDLE)" +log "las tablets toman el cambio con una recarga; el index.html se sirve con no-cache." diff --git a/deploy/rollback-frontend.sh b/deploy/rollback-frontend.sh new file mode 100755 index 0000000..3b584dd --- /dev/null +++ b/deploy/rollback-frontend.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Vuelve el frontend a la release anterior. Uso: ./deploy/rollback-frontend.sh +# +# A diferencia del backend, aquí el rollback SIEMPRE funciona: no hay migraciones +# de por medio, solo archivos estáticos. Es mover un symlink, sin recargar nada. +set -euo pipefail + +DEPLOY_DIR="$HOME/deploys/restfood-frontend" +URL_VERIFICACION="https://192.168.10.100/" + +log() { echo "[rollback-front] $*"; } +die() { echo "[rollback-front] ERROR: $*" >&2; exit 1; } + +[[ -e "$DEPLOY_DIR/previous" ]] || die "no hay release anterior a la que volver" + +ACTUAL="$(readlink -f "$DEPLOY_DIR/current")" +ANTERIOR="$(readlink -f "$DEPLOY_DIR/previous")" +[[ "$ACTUAL" != "$ANTERIOR" ]] || die "current y previous apuntan a la misma release" + +log "actual: $(basename "$ACTUAL")" +log "volviendo a: $(basename "$ANTERIOR")" + +# Se intercambian: así un segundo rollback deshace el primero. +ln -sfn "$ANTERIOR" "$DEPLOY_DIR/current" +ln -sfn "$ACTUAL" "$DEPLOY_DIR/previous" + +CODIGO="$(curl -s -k -o /dev/null -w '%{http_code}' -m 10 "$URL_VERIFICACION" || true)" +SERVIDO="$(curl -s -k -m 10 "$URL_VERIFICACION" | grep -oE 'assets/index-[A-Za-z0-9_-]+\.js' | head -1 || true)" +[[ "$CODIGO" == "200" ]] || die "tras el rollback el sitio responde HTTP $CODIGO — revisar a mano" + +log "listo: HTTP $CODIGO, sirviendo $SERVIDO" +log "las tablets que tengan la pantalla abierta necesitan una recarga."