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

165 lines
7.1 KiB
Markdown
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 | **Sí** |
| WSL → Hetzner | 74 s | **Sí** |
| Saneo de collations + exclusión `*_bak` | 2 s | **Sí** |
| Import en MySQL 8.4 | 107 s | **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.
```bash
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
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):
```bash
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.
```bash
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
```bash
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
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
```bash
# 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.