Arrastrar temas por FTP en 2026 suele acabar igual: falta un archivo de inc/, el composer.lock viene de otro portátil y producción cae porque alguien sobrescribió wp-config.php. La Integración Continua y el Despliegue Continuo (CI/CD) no son un capricho de grandes empresas: son la forma de que cada cambio en Git pase por el mismo build, las mismas pruebas y el mismo camino hasta el servidor.
Esta guía trata de WordPress con un tema o plugin en el repositorio (Composer, npm y, si quieres, Docker), no de un sitio que solo existe en el panel. Si el código personalizado vive solo en la base de datos y en la biblioteca de medios, primero lleva temas y código propio a Git: sin eso, CI/CD no tiene nada que construir.
Documentación del runner: GitHub Actions. En paralelo conviene endurecer el servidor según Hardening WordPress. Para los comandos posteriores al despliegue, WP-CLI.
Por qué no desplegar WordPress por FTP
El despliegue manual es lento, inconsistente y arriesgado. Tres problemas aparecen una y otra vez en tiendas WooCommerce y en traspasos entre agencias:
- Entornos distintos: en local PHP 8.2 y Composer 2.7, en el VPS PHP 8.1 sin
intl. Elcomposer installlocal “funciona”; producción cae con un class not found. - Sin trazabilidad: tras un incidente nadie sabe qué commit estaba en producción. El panel del hosting muestra la fecha de un archivo, no un SHA de Git.
- Secretos en disco: contraseñas SFTP guardadas en FileZilla o un
.envsubido al repositorio “solo un momento” siguen siendo una vía clásica de fuga.
Al subir archivos a mano dependes de tu memoria para que cada archivo llegue al sitio correcto, con los permisos correctos y en el orden correcto. Los fallos típicos son sobrescribir la configuración de producción con la de desarrollo, olvidar dependencias actualizadas o mezclar archivos de ramas distintas. El protocolo FTP, además, envía las credenciales en texto plano; SFTP lo corrige, pero las credenciales guardadas en portátiles siguen siendo un objetivo para el malware.
CI/CD lo resuelve de forma mecánica: build sobre una imagen conocida (shivammathur/setup-php o tu propio Docker), artefacto versionado por SHA y despliegue por SSH con una clave guardada en Secrets. Cada despliegue queda ligado a un commit, a un pull request y a un registro del pipeline, y los pasos son idénticos lo ejecute quien lo ejecute.
Cómo desplegar WordPress con GitHub Actions
Un workflow típico para un tema propio con plugins vía Composer:
- Disparador: push a
maino fusión de un pull request revisado.developsuele desplegar solo a staging. - Build:
composer install --no-dev --optimize-autoloader,npm ci,npm run build. Fija PHP y Node en el workflow (php-version: '8.3',node-version: '24'). - Pruebas: PHPUnit para plugins y, si procede, Playwright en el flujo de compra. Si fallan, el job se detiene y no hay despliegue.
- Artefacto: empaqueta una release sin
.gitninode_modules, convendor/yassets/dist/ya compilados. - Despliegue: rsync o SCP a
releases/<run_id>/y después el cambio atómico del enlace simbólico.
Fija versiones que aún reciben correcciones. PHP 8.2 recibe parches de seguridad solo hasta el 31 de diciembre de 2026 y PHP 8.3 hasta el 31 de diciembre de 2027 (versiones soportadas de PHP). Node.js 20 llegó al final de su vida el 30 de abril de 2026; Node.js 22 tiene soporte hasta el 30 de abril de 2027 y Node.js 24 hasta el 30 de abril de 2028 (calendario de versiones de Node.js). La versión menor de PHP sigue a producción, no a la última publicada.
Esbozo corto en GitHub Actions, con etiquetas de versión mayor para que se lea bien; la sección sobre cómo proteger el workflow, más abajo, explica por qué en producción se fijan SHAs de commit:
name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci && npm run build
- name: rsync release
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
# escribir la clave, rsync a releases/$GITHUB_SHA, ln -sfnGuarda el script completo del enlace simbólico en el repositorio como bin/release.sh y llámalo desde el workflow. Así el mismo script funciona desde un portátil cuando Actions no está disponible.
Qué runner de GitHub usar para WordPress
GitHub ofrece runners alojados con Ubuntu, Windows y macOS. Para WordPress lo habitual es Ubuntu, porque se parece más al servidor de producción típico. Los runners autoalojados ejecutan el pipeline en tu propia infraestructura cuando necesitas otro hardware o acceso a una red privada.
Cómo evitar dos despliegues de WordPress a la vez en GitHub Actions
Dos fusiones rápidas a main arrancan dos ejecuciones del workflow. Sin bloqueo, ambas hacen rsync a releases/ y cambian current en un orden que nadie eligió. Un grupo concurrency en GitHub Actions permite como mucho un workflow en ejecución por grupo; una ejecución recién encolada espera como pending (documentación de GitHub: controlar la concurrencia).
En un despliegue cuentan dos detalles. Por defecto, un grupo mantiene una ejecución activa y una pendiente, y una ejecución más nueva cancela la pendiente más antigua: gana el último commit y el del medio se salta. cancel-in-progress: true cancelaría también el job en curso, lo que puede cortar el rsync a mitad de un directorio de release. En producción déjalo en false y usa true en los workflows de pruebas de los pull requests, donde solo importa el commit más reciente.
concurrency:
group: deploy-production
cancel-in-progress: falseComo el enlace simbólico cambia solo después de un rsync completo, una ejecución pendiente cancelada nunca toca current.
Cómo proteger producción con entornos de GitHub Actions
Añade environment: production al job de despliegue y configura ese entorno en los ajustes del repositorio. Se aplican tres protecciones (documentación de GitHub: despliegues y entornos):
- Revisores obligatorios: hasta seis usuarios o equipos, y una aprobación libera el job. Con el bloqueo de autoaprobación activo, quien lanzó el despliegue no puede aprobarlo.
- Ramas y etiquetas de despliegue: solo
maino las etiquetas de release pueden desplegar aproduction, así que un workflow en una rama de funcionalidad no llega al servidor de producción. - Secretos de entorno: disponibles solo para los jobs que referencian el entorno y, con revisores obligatorios, solo después de la aprobación. El
DEPLOY_SSH_KEYde producción va aquí, no en los secretos del repositorio.
Cada entorno (desarrollo, staging, producción) tiene así su propio juego de secretos y variables, y los de producción no son accesibles desde staging. Revisa el plan antes de contar con ello. En GitHub Free, Pro y Team, los revisores obligatorios y los temporizadores de espera solo funcionan en repositorios públicos. Las ramas de despliegue y los secretos de entorno funcionan en repositorios privados con Pro y Team, pero no con Free. Los repositorios WordPress de agencia suelen ser privados, así que en esos planes no hay puerta de aprobación y la protección recae en las reglas de rama.
Cómo proteger el workflow de despliegue en GitHub Actions
El job de despliegue guarda una clave SSH de producción, así que el archivo del workflow merece la misma revisión que el código PHP. La referencia de GitHub sobre uso seguro recoge las medidas que importan aquí:
- Fija las actions de terceros al SHA completo del commit. Una etiqueta como
@v2se puede mover o borrar. Redirigir un SHA exigiría una colisión SHA-1. Toma el SHA del repositorio de la propia action, no de un fork. GitHub señala ahora un lockfile de dependencias del workflow como la opción más fuerte para los repositorios que lo adopten. - Da a
GITHUB_TOKENlo mínimo. Ponpermissions: contents: readarriba y amplía por job solo donde un paso lo necesite. Un despliegue por rsync y SSH solo necesita leer el repositorio. - No metas entradas no confiables directamente en
run:. El título de un pull request o el nombre de una rama dentro de${{ }}en un script de shell abre la puerta a la inyección de código. Pasa el valor por una variable de entorno intermedia. - Un secreto por valor. Los datos estructurados, como un bloque JSON, pueden romper el enmascarado en los logs, así que guarda host, usuario, ruta y clave como secretos separados.
La misma página indica que el enmascarado solo cubre los secretos usados en un job y accesibles para el runner, así que nunca hagas echo de valores derivados, como una clave decodificada.
Despliegue atómico de WordPress sin tiempo de inactividad
Sobrescribir archivos en su sitio dentro de public_html hace que algunas peticiones vean durante un momento un functions.php viejo con una plantilla nueva. En WooCommerce eso puede romper sesiones de carrito en plena campaña.
Estructura atómica:
- base:
/var/www/site/ - releases:
/var/www/site/releases/20260920-142211/ - shared: uploads,
wp-config.php, caché de objetos, fuera de la release current: enlace simbólico a la release activa
Tras un rsync correcto:
ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/currentEl root de Nginx apunta a .../current/public (o su equivalente). El cambio es inmediato. Borra las releases antiguas pasados N días o conserva las tres últimas para volver atrás.
Los uploads de WordPress tienen que vivir fuera de la release; si no, cada despliegue borra medios o copia gigas. El patrón clásico: shared/uploads enlazado en wp-content/uploads de cada release. Volver atrás es lo mismo al revés: apuntar current a la release anterior.
Entorno staging de WordPress y estrategias de despliegue comparadas
En 2026, el camino desde una rama de funcionalidad por un pull request y staging hasta main y producción no es opcional para tiendas y webs de captación. El entorno de staging debe compartir con producción:
- la misma versión de PHP y las mismas extensiones
- una copia de la base de datos (anonimizada si hay datos personales bajo el RGPD)
- los secretos de pago solo en modo sandbox
Las diferencias entre staging y producción son la fuente más común de errores del tipo “funciona en staging, pero no en producción”. Las URL de previsualización por pull request ayudan en rediseños y revisiones de CSS, sobre todo en agencias donde el cliente aprueba cambios visuales antes de publicarlos; las migraciones de esquema siguen necesitando staging con un volcado real.
| Estrategia | Riesgo | Coste de mantenimiento | Cuándo encaja |
|---|---|---|---|
| FTP manual | Muy alto | Arranque barato, caídas caras | Solo pruebas o aprendizaje |
| Hook de Git en el servidor | Medio | Bajo | Portfolio pequeño, un desarrollador |
| CI + rsync atómico | Bajo | Medio | Webs de negocio, WooCommerce, agencias |
| Blue/green o dos VPS | Mínimo | Alto | Mucho tráfico, SLA, pagos |
En blue/green mantienes dos entornos de producción idénticos: uno sirve el tráfico y el otro recibe la versión nueva; cuando está verificada, el balanceador cambia el tráfico. Un despliegue canary manda primero la versión nueva a una parte pequeña del tráfico y la amplía mientras los errores se mantengan bajos. Ambos exigen infraestructura duplicada y rara vez compensan por debajo de cientos de sesiones simultáneas. A la mayoría de tiendas medianas les basta con el enlace simbólico atómico y la vuelta atrás por directorio.
Pruebas automatizadas en un pipeline CI/CD de WordPress
Un pipeline sin pruebas solo envía código roto más deprisa. Cuatro capas encajan en un repositorio WordPress, de la más rápida a la más lenta:
- Pruebas unitarias con PHPUnit: comprueban funciones sueltas de un plugin o tema sin base de datos. Se ejecutan en cada push.
- Pruebas de integración: cargan WordPress contra una base de datos de pruebas temporal.
wp scaffold plugin-testsgenera la estructura:phpunit.xml.dist,bin/install-wp-tests.shpara la suite de pruebas y la base de datos,tests/bootstrap.php, una prueba de ejemplo y un conjunto de reglas.phpcs.xml.dist. El archivo de CI por defecto es para CircleCI;--ci=githubescribe en su lugar una configuración para GitHub Actions (WP-CLI: wp scaffold plugin-tests). - Análisis estático con PHPStan y PHP_CodeSniffer: detecta errores de tipos e infracciones del estándar de código sin ejecutarlo.
- Pruebas end-to-end con Playwright: recorren flujos reales de usuario, como un formulario de contacto o el carrito y el checkout de WooCommerce. Son la capa más lenta, así que ejecútalas en los pull requests a
mainy contra staging, no en cada commit.
Un job en rojo en cualquier capa detiene el workflow antes del paso del artefacto, y nada llega al servidor.
Cómo gestionar secretos y migraciones de base de datos en el CI/CD de WordPress
Nunca subas al repositorio:
- claves SSH
- contraseñas de base de datos
AUTH_KEY/SECURE_AUTH_KEYdewp-config.php- tokens de API de pasarelas de pago
En GitHub están en Settings, luego Secrets and variables, luego Actions. Se inyectan como env en el paso de despliegue, así que aunque el repositorio se filtre, el servidor sigue protegido. En el servidor, wp-config.php vive en shared/ y no forma parte del artefacto de CI.
WP-CLI tras el cambio del enlace simbólico:
wp cache flushsi hay caché de objetoswp rewrite flushdespués de cambiar CPTwp plugin listcomo línea de smoke en el log
Cómo ejecutar WP-CLI en el servidor de producción desde el runner
Estos comandos tienen que ejecutarse en el servidor, contra la release nueva. WP-CLI lo hace desde el runner con el parámetro global --ssh ([<scheme>:][<user>@]<host>[:<port>][<path>]) o con alias en el wp-cli.yml del proyecto (manual de WP-CLI: ejecutar comandos en remoto):
@staging:
ssh: [email protected]/var/www/site/current/public
@prod:
ssh: [email protected]/var/www/site/current/publicCon ese archivo en el repositorio, wp @prod cache flush se ejecuta en producción. WP-CLI tiene que estar instalado igualmente en el servidor y accesible como wp: el runner solo pasa el comando por SSH.
Otros comandos útiles en el paso posterior al cambio, todos en la referencia de comandos de WP-CLI:
wp core is-installedno imprime nada y responde con el código de salida, así que una comprobación fallida hace fallar el paso.wp core update-db --dry-runcompara las versiones de la base de datos antes delwp core update-dbreal tras una actualización del núcleo.wp core verify-checksumscompara los archivos del núcleo con las sumas de comprobación de WordPress.org y detecta archivos cambiados fuera de Git.wp maintenance-mode activateydeactivatecubren la release poco frecuente que debe cambiar el esquema mientras nada escribe en él.
Planifica las migraciones de base de datos aparte del despliegue de archivos. Volver atrás el enlace simbólico no deshace un ALTER TABLE. En migraciones destructivas: primero copia de seguridad (wp db export en staging y producción), luego la migración y solo entonces el tráfico al código que necesita el esquema, o bien un feature flag en PHP.
Documentación de dependencias PHP: Composer install. En CI, siempre --no-dev para producción y un composer.lock bloqueado.
Errores comunes en un pipeline de despliegue de WordPress
En VPS (DigitalOcean, Linode, Hetzner) y en hosting gestionado con SSH se repiten estos fallos:
.gitignorese comevendor/: producción recibe un tema sin autoloader. O construyes vendor en el runner y lo incluyes en el artefacto, o ejecutas Composer en el servidor después del rsync (y entonces el servidor necesita Composer y acceso a Packagist).- Rutas equivocadas: el script supone
/var/www/htmly el hosting usa/home/user/domains/.... Fija las rutas en Secrets por entorno. - Opcache conserva el código viejo: después de
ln -sfn, recarga PHP-FPM o llama aopcache_resetmediante WP-CLI o un mu-plugin, solo para el usuario de despliegue. - Cron y colas: el Action Scheduler de WooCommerce puede ejecutar tareas con el código viejo durante unos minutos; tras una release grande, vigila un rato las tareas fallidas.
Smoke test tras el despliegue (curl desde el runner o en un job aparte):
- portada con HTTP 200
/wp-login.phpcon 200- una URL crítica de producto o de formulario
- opcionalmente un HEAD a la CDN si los assets llevan hash en el nombre
Si el smoke test falla, volver automáticamente al enlace simbólico anterior cuesta menos que la llamada del cliente a las 23:00.
Cómo desplegar temas de bloques y mu-plugins con Git
No toda la superficie de WordPress cabe en el mismo pipeline. El contenido en la base de datos (entradas, grupos de campos ACF si no exportas JSON, CSS de Elementor) sigue fuera de la release. CI/CD protege el código; no sustituye a las copias de seguridad de la base de datos.
Divisiones prácticas:
- Tema clásico o híbrido con Vite: build completo en CI, artefacto con
style.css+dist/. - Tema de bloques (FSE): guarda
theme.jsony las plantillas HTML en Git; los cambios hechos en el editor del sitio vuelven por pull request, o producción y Git divergen tras el primer “Guardar”. - Must-use plugins: viajan con la release; no se actualizan desde el panel. Buen sitio para comprobaciones de salud del despliegue y para desactivar el editor de archivos.
- Plugins de wordpress.org: fija versiones con Composer (
wpackagist) o actualízalos en un job revisado. Mezclar “Actualizar en el panel” con “CI sobrescribióplugins/” deja un árbol de plugins aleatorio.
Las pilas de agencia suelen mezclar un tema propio, WooCommerce desde wpackagist y dos plugins premium en zip instalados con un repositorio path de Composer. El pipeline debe conocer las tres fuentes, o staging y producción acabarán diferenciándose en un zip de hace seis meses.
Lista de comprobación para el despliegue continuo de WordPress
Antes de conectar main a producción, recorre esta lista una vez:
- Repositorio privado, o confirma que el historial no contiene secretos (
git log -pno muestra.envantiguos). - Protección de rama: pull request obligatorio y workflow
testen verde obligatorio. - Secrets separados:
DEPLOY_HOST_STAGING,DEPLOY_HOST_PRODy claves SSH distintas. - Staging con la misma versión de PHP que producción (no “nosotros usamos 8.3 y el cliente está en 8.1”).
- Copia de la base de datos antes del primer cambio atómico, aunque la vuelta atrás del código esté lista.
- Monitorización de disponibilidad (UptimeRobot, Better Stack o similar) con aviso por SMS o Slack sobre la URL de producción.
- Un runbook de una página: cómo hacer a mano
ln -sfna la release anterior y quién tiene acceso al servidor.
Solo entonces activa el despliegue automático desde main. Hasta ese momento basta con “build + pruebas” en los pull requests y un workflow_dispatch manual a staging.
Conclusión
CI/CD para WordPress no es un “YAML mágico”. Es un contrato repetible: el mismo PHP, el mismo lockfile, el mismo artefacto y un cambio sin ventana de archivos a medio subir. Empieza por Git y staging, añade Actions para los builds de Composer y npm, y termina con un current atómico y un smoke test. El FTP pasa a ser una herramienta de emergencia, no el proceso.
Si necesitas un desarrollador WordPress que mantenga ese pipeline junto al código del tema de una tienda o una web de captación, WPPoland trabaja con una pila real. Para reforzar la parte de seguridad, revisa también la auditoría de seguridad WordPress.







