Files
feadulta/docs/cutover/RUNBOOK-cutover-wordpress.md
T
rafa 508a6b4178 cutover: runbook del lunes, script de seguridad post-import y sitio E2E de staging
Con los tiempos REALES medidos en el ensayo del 31-jul, no estimados:
dump 25s + subida 74s + saneo 2s + import 107s = camino critico ~3min30s.

- docs/cutover/RUNBOOK-cutover-wordpress.md: los pasos en orden, copiables, con
  el rollback y los tres verdes obligatorios previos (freeze, CDMON vivo, modo
  SSL de la zona).
- scripts/cutover/post-import-seguridad.sh: rota la contrasena de los 6
  administradores (las lee del .env del perfil, nunca van en el repo), retira el
  rol a pabloarias/josek/andrey y limpia los cron huerfanos de Yoast y WP
  Profile Builder. Es un script y no una tarea manual porque el dump revive esas
  cuentas en CADA import.
- tools/e2e/sites/nuevo.json: la suite toma el host de un JSON, asi que apuntarla
  al staging es anadir un fichero.

Hallazgo del ensayo: el origen es MariaDB 11.8.6 y el destino MySQL 8.4.10; el
dump trae 20 tablas con collations uca1400_* que MySQL rechaza. El saneo las
mapea antes de importar. Sin ese paso, el import aborta.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:15:58 -04:00

7.1 KiB
Executable File

Runbook del cutover — WordPress feadulta.com: CDMON → Hetzner

Fecha prevista: lunes 3 de agosto de 2026. Plan maestro: gitea rafa/feadulta#180. Todos los tiempos de abajo están medidos en el ensayo del 31-jul, no estimados.


Antes de empezar (verde obligatorio)

  • Freeze editorial avisado a Inma. El cutover no puede caer el día de publicación de la carta.
  • CDMON sigue vivo (§1). Es el rollback: si no está, no se ejecuta.
  • Modo SSL de la zona en Cloudflare = Full (strict). Si está en Flexible, Cloudflare habla HTTP con el origen mientras Traefik fuerza HTTPS → bucle de redirección y sitio caído. Nuestro token no puede leer este ajuste: lo confirma Inma.
  • Inventario del wildcard. La zona tiene *.feadulta.com → apex, proxied. Al mover el A del apex, todo subdominio no declarado se va a Hetzner y Traefik devolverá su 404. Comprobado pendiente: info.feadulta.com.

Tiempos medidos (ensayo del 31-jul)

Fase Medido ¿Se repite el lunes?
Dump CDMON → WSL (105 MB) 25 s
WSL → Hetzner 74 s
Saneo de collations + exclusión *_bak 2 s
Import en MySQL 8.4 107 s
Instalar plugins + tema + 28 mu-plugins 4 min 19 s No (ya hecho)
Pre-sync de uploads 5,9 GB ver #180 No (ya hecho; el lunes solo el delta)
Camino crítico de la ventana ≈ 3 min 30 s + delta de uploads

El corte real percibido por el visitante es solo el cambio del A record, que en Cloudflare es instantáneo. Todo lo de arriba se hace antes, con el sitio viejo aún sirviendo.


1. Backup de última hora en el origen

No fiarse del cron de UpdraftPlus de las 05:09: forzar uno nuevo o confirmar que el del día existe.

sshpass -p "$FEA_PROD_SSH_PASS" ssh feadulta@134.0.10.170 \
  'ls -lt /web/wp-content/updraft/*-db.gz | head -3'

2. Traer el dump del día

bash scripts/cutover/01-traer-dump.sh        # CDMON → WSL → Hetzner, con sha256 en los 3 puntos

Dos saltos a propósito: scp/sftp no funcionan contra la jaula de CDMON, y no queremos dejar la contraseña de CDMON escrita en el Hetzner.

3. Sanear el dump ⚠️ el paso que no se puede saltar

El origen es MariaDB 11.8.6 y el destino MySQL 8.4.10. El dump trae 20 tablas con collations uca1400_* que solo existen en MariaDB 11+; MySQL las rechaza en el CREATE TABLE y el import aborta.

utf8mb4_uca1400_ai_ci  →  utf8mb4_0900_ai_ci
utf8mb3_uca1400_ai_ci  →  utf8mb4_0900_ai_ci   (+ CHARSET=utf8mb3 → utf8mb4)
utf8mb4_unicode_520_ci →  sin tocar (válida en MySQL 8.4)

También excluye las 5 tablas de respaldo (wp_options_bak, wp_posts_bak, wp_postmeta_bak, wp_usermeta_bak, wp_fea_date_slug_posts_backup).

Verificación antes de importar (las tres deben dar 0):

grep -c 'uca1400' feadulta-saneado.sql
grep -c 'utf8mb3'  feadulta-saneado.sql
grep -oE '^CREATE TABLE `[^`]+`' feadulta-saneado.sql | grep -cE '_bak|backup'

4. Importar

--default-character-set=utf8mb4 en el CLIENTE es obligatorio. Omitirlo es exactamente lo que produjo el mojibake ’ en summaraise.

docker exec -i mysql-r2ssjifwj0r0ghyoqd528uaa \
  mysql --default-character-set=utf8mb4 -uroot -p"$RP" wordpress < feadulta-saneado.sql

Verificación: 29 tablas, y las filas deben cuadrar con (líneas que empiezan por " (") + (nº de sentencias INSERT) de cada tabla — la primera fila de cada INSERT va pegada al VALUES. Ignora el # Approximate rows expected del dump: es la estimación de InnoDB y para wp_posts dice 64.160 cuando las filas reales son 43.941.

5. Delta de uploads

rsync -a --info=progress2 --stats --exclude='*.php' \
  -e "sshpass -p ... ssh" feadulta@134.0.10.170:/web/wp-content/uploads/ <destino>/

Solo mueve lo publicado desde el pre-sync. --delete únicamente tras revisar un --dry-run.

6. Higiene de seguridad — SIEMPRE después de cada import

bash scripts/cutover/post-import-seguridad.sh

El dump trae las cuentas de administrador con sus hashes, incluidas las tres deshabilitadas en el incidente #183: reimportar las revive. El script rota la contraseña de los 6 administradores (leyéndolas de ~/.hermes/profiles/feadulta/.env), retira el rol a pabloarias/josek/andrey, y limpia los cron huérfanos de Yoast y WP Profile Builder.

Doble cinturón: además del rol retirado, el mu-plugin fea-security-blocked-users.php bloquea wp_authenticate_user y allow_password_reset para esos tres IDs. Va en el manifiesto de docs/cutover/mu-plugins-manifest.txt, no en un glob fea-* — un glob se lo dejaría fuera.

7. Quitar el WP_HOME/WP_SITEURL del staging

Coolify → servicio feadulta-wp → borrar la variable WORDPRESS_CONFIG_EXTRA → redeploy

No hace falta ningún search-replace: la base de datos ya trae siteurl/home = https://www.feadulta.com, que es la URL final. La constante era solo para el staging. Esto ahorra la pasada más lenta de todo el proceso.

8. Cambiar el A record en Cloudflare

# feadulta.com y www.feadulta.com → 188.40.120.157, MANTENIENDO proxied=true (naranja)

El token sí tiene permiso de escritura, verificado el 31-jul creando nuevo.feadulta.com. Después: purgar la caché de Cloudflare.

9. Verificación en caliente

  • Portada en los 5 idiomas (/, /en/, /fr/, /it/, /pt/) — /es/ responde 301, es lo normal.
  • Carta de la semana, evangelios, EFFA, autores con avatar.
  • Buscador (índice FULLTEXT) devuelve resultados.
  • Audio TTS suena.
  • Imágenes cargan (depende del delta de uploads).
  • wp-login.php entra con la contraseña rotada, y las 3 cuentas del incidente NO entran.
  • Certificado Let's Encrypt para apex y www.
  • Cero errores PHP en la portada y en docker logs.
  • GA4 en tiempo real.

10. Application passwords — con Inma delante, no antes

Solo existe una en todo el sitio: publicabot, del usuario icalvotorre (ID 1048), creada el 27-jun-2026, último uso el 29-jul-2026 desde 213.94.23.1. Es la que usan los bots de la carta.

Rotarla sin coordinar para la publicación de la carta semanal y nadie sabrá por qué. Se rota con Inma delante, generando la nueva y actualizándola en el bot en el mismo momento.

(Rotar la contraseña de un usuario no revoca sus application passwords: el paso 6 no la afecta.)


Rollback

Revertir el A record en Cloudflare + purgar caché. Segundos. El Joomla/WordPress de CDMON sigue intacto y sirviendo — comprobado con curl --resolve contra 134.0.10.170.

Esto solo funciona mientras CDMON siga vivo. Es la razón por la que el §1 no es opcional.

Después (no el mismo día)

  • Backups en Hetzner: hoy NO HAY NINGUNO. La tabla scheduled_database_backups de Coolify está vacía y no hay cron. Summaraise lleva semanas así. Hay que darle dueño.
  • Actualizar los scripts y guías que apuntan a 134.0.10.170 y a /web/ (cierra #158).
  • Retirar nuevo.feadulta.com de Cloudflare cuando ya no haga falta.