Reserva sin pagar · confirmamos stock por WhatsApp · entregas en Lima WhatsApp 920 166 046
Deja de copiar y pegar comandos: despliega tu WordPress automáticamente con Claude y GitHub Actions

Deja de copiar y pegar comandos: despliega tu WordPress automáticamente con Claude y GitHub Actions

ITALO TECNOLOGÍA Software · CI/CD

Despliegue continuo · Claude + GitHub Actions + WordPress

Deja de copiar y pegar comandos: despliega tu WordPress automáticamente con Claude y GitHub Actions

Si programas con ayuda de una IA y todavía subes archivos con WinSCP y ejecutas scripts en PuTTY, esta guía es para ti: un pipeline donde la IA programa, GitHub despliega y tú solo revisas el resultado en la web. Arquitectura completa, seguridad explicada y comandos para replicarlo.

7 pasos
manuales por cambio
descargar, WinSCP, PuTTY, copiar la salida, pegarla en el chat
1 tarde
para montarlo
5 pasos de configuración, una sola vez
6 s
por despliegue
desde que GitHub recibe el cambio hasta el VPS
0
comandos pegados
en cada cambio nuevo, ni de código ni de base de datos

El problema: tú eres el cable entre la IA y el servidor

Es el ciclo habitual al programar con cualquier IA: le pides un cambio, te da el código, lo descargas, lo subes al servidor con WinSCP, entras por PuTTY a ejecutar el script, copias lo que sale en la terminal y lo pegas de vuelta en el chat para que te diga el siguiente paso. Cada cambio pequeño son siete pasos manuales, y en cada uno puedes equivocarte de carpeta, pegar un bloque incompleto o perder el hilo.

La IA escribe el código, pero quien lo mueve de un lado a otro eres tú. Ese es el cuello de botella.

Sin automatizar · por cada cambio

  1. Pides el cambio en el chat
  2. La IA te da el código
  3. Lo descargas a tu PC
  4. Lo subes con WinSCP
  5. Lo ejecutas en PuTTY
  6. Copias la salida de la terminal
  7. La pegas en el chat y vuelta a empezar

Con este pipeline · por cada cambio

  1. Le dices a Claude qué necesitas
  2. Claude escribe el código y lo sube a GitHub
  3. GitHub Actions lo despliega solo en el VPS
  4. Revisas el resultado en tu web

Qué vas a montar

Tiene nombre en la industria: un pipeline de despliegue continuo (la «CD» de CI/CD). Cada vez que el código cambia en la rama principal de GitHub, un proceso automático lo lleva al servidor sin intervención humana. Esta guía le suma una segunda pieza que casi nunca aparece en tutoriales de WordPress: migraciones de base de datos con WP-CLI, para que los cambios de contenido —categorías, extractos, ajustes— también viajen por el mismo camino y queden registrados en git.

Las piezas son tres: Claude (la IA de Anthropic) con acceso a tu repositorio de GitHub, GitHub Actions como motor de despliegue y tu VPS con una puerta de entrada diseñada para hacer una sola cosa.

Lo que quedó automatizado: cambios de código del tema (PHP, CSS, JS) y cambios en la base de datos de WordPress mediante scripts WP-CLI versionados. Cada despliegue verifica al final que la web responda. El primer resultado fue el listado de este blog, desplegado y corregido así.
Lo que sigue siendo manual, a propósito: modificar el script que corre como root en el servidor y revertir cambios de base de datos (para eso está el backup). En la pestaña Seguridad está el porqué: esos dos límites son una decisión de diseño, no una carencia.

El recorrido de un cambio

Así viaja un cambio desde que lo pides hasta que aparece en la web. Claude nunca toca el servidor directamente: su acceso termina en GitHub.

01Túdescribes el cambio en el chat
02Claudeprograma, prueba y hace git push
03GitHubrecibe el commit en main
04GitHub Actionsse conecta por SSH con una llave restringida
05VPSgit pull + migraciones WP-CLI
06Web en vivoActions comprueba que responda 200

Las piezas y dónde vive cada una

PiezaQué haceDónde vive
RepositorioGuarda la carpeta wp-content: el tema hijo, los scripts de despliegue y las migracionesGitHub (privado)
WorkflowDefine cuándo desplegar y cómo conectarse al servidor.github/workflows/deploy.yml en el repo
SecretsIP, puerto, llave privada y huella del servidor, cifradosConfiguración del repo en GitHub
Usuario de despliegueLa única puerta por la que entra GitHub; no tiene contraseña ni shellVPS
Script de despliegueHace el git pull, corrige permisos y corre las migraciones pendientesVPS, fuera del repo, propiedad de root
MigracionesScripts WP-CLI numerados que cambian la base de datos una sola vezops/migrations/ en el repo

El workflow de GitHub Actions

Este es el archivo completo. Se dispara solo cuando cambia el tema o los scripts de ops/, y también se puede lanzar a mano desde la pestaña Actions. Los valores sensibles nunca aparecen en el archivo: se leen de los secrets.

name: Deploy theme to VPS

on:
  push:
    branches: [main]
    paths:
      - 'themes/mi-tema-hijo/**'
      - 'ops/**'
  workflow_dispatch:

concurrency:
  group: deploy-vps
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Configurar SSH
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          printf '%s\n' "${{ secrets.VPS_KNOWN_HOSTS }}" > ~/.ssh/known_hosts

      - name: Desplegar en el VPS
        run: |
          ssh -i ~/.ssh/deploy_key -p "${{ secrets.VPS_PORT }}" \
              -o StrictHostKeyChecking=yes -o BatchMode=yes \
              deploy-github@"${{ secrets.VPS_HOST }}" deploy

      - name: Verificar que el sitio responde
        run: |
          code=$(curl -s -o /dev/null -w '%{http_code}' https://tusitio.com/blog/)
          test "$code" = "200"

concurrency evita que dos despliegues se pisen, StrictHostKeyChecking=yes impide conectarse a un servidor falso y el último paso hace que el job falle si la web deja de responder.

¿Por qué el repo es wp-content y no todo WordPress? Porque el núcleo de WordPress se actualiza solo y wp-config.php tiene las contraseñas de la base de datos. Versionar únicamente wp-content deja fuera lo que nunca debe estar en git, y además un .gitignore excluye uploads/ y los backups.

Darle a una máquina la llave de tu servidor

Automatizar un despliegue significa que un sistema externo —GitHub— puede entrar a tu servidor sin que estés mirando. La pregunta correcta no es «¿funciona?», sino «¿qué es lo peor que puede pasar si esa llave se filtra?». Todo el diseño gira alrededor de que la respuesta sea «casi nada».

La idea se llama principio de mínimo privilegio: cada pieza recibe exactamente el permiso que necesita para su tarea, y ni uno más.

Ocho capas entre GitHub y tu servidor

  1. Secrets cifradosLa llave privada vive cifrada en GitHub. No aparece en el código ni en los logs.
  2. Huella del servidor verificadaGitHub solo se conecta si el servidor presenta la huella registrada (known_hosts). Si alguien suplanta el servidor, la conexión se corta.
  3. Usuario dedicado sin contraseñaGitHub no entra como root ni como tu usuario: entra como deploy-github, que tiene la contraseña bloqueada y solo acepta esa llave.
  4. Llave con restrictSin terminal, sin túneles, sin reenvío de puertos. Aunque la llave se filtre, no sirve para abrir una sesión.
  5. Comando forzadoLo que sea que pida quien se conecte, el servidor ejecuta siempre lo mismo: el script de despliegue. Nada más.
  6. sudo para un solo comando, sin argumentosEl usuario de despliegue puede elevar permisos únicamente para ese script y sin pasarle parámetros.
  7. Script de root fuera del repositorioQuien pueda escribir en GitHub no puede cambiar lo que corre como root. Ese script se instala a mano en el servidor.
  8. Migraciones como www-dataLos cambios de base de datos corren con el mismo usuario que ya usa WordPress, nunca como root.

El error de diseño más común

La tentación es poner el script de despliegue dentro del repositorio y ejecutarlo como root. Funciona, pero tiene un problema serio: cualquiera con permiso de escritura en el repo —incluido alguien que robe un token de GitHub— puede modificar ese script y en el siguiente despliegue ejecutar lo que quiera como root. Escribir en el repo equivale a ser root en el servidor.

Además, si tu servidor tiene el acceso de root por SSH deshabilitado (PermitRootLogin no, como debería), ese diseño ni siquiera funciona. El diseño correcto es un usuario dedicado, un único comando con sudo y el script de root viviendo fuera del repo. Así, lo peor que puede hacer alguien con acceso al repo es lo mismo que ya podría hacer el código PHP del tema: actuar como www-data.

Lo que nunca deberías publicar: la IP de tu servidor, el puerto SSH, el nombre del usuario de despliegue y las rutas reales. Por eso todos los comandos de este artículo usan valores de ejemplo (203.0.113.10, puerto 2222, usuario deploy-github, dominio tusitio.com). Reemplázalos por los tuyos y guárdalos solo en los secrets de GitHub.
Un detalle más: la llave privada se genera en el servidor, se copia una vez a los secrets de GitHub y se borra del servidor con shred. No queda guardada en ningún lugar más que cifrada en GitHub.
Todo esto se hace una sola vez, como root (o con sudo) en el servidor. Entorno de referencia: Ubuntu, Apache con PHP-FPM 8.3 corriendo como www-data y WordPress en /var/www/tusitio.com, con wp-content ya clonado desde GitHub. Cambia los valores de ejemplo por los tuyos.
1

Crear el usuario de despliegue

Un usuario solo para GitHub, con la contraseña bloqueada. El último comando muestra tu configuración de SSH: si tienes AllowUsers o AllowGroups, agrega ahí el usuario nuevo.

id deploy-github 2>/dev/null || useradd -m -s /bin/bash deploy-github
passwd -l deploy-github
sshd -T | grep -iE '^(port|permitrootlogin|pubkeyauthentication|allowusers|allowgroups) '
2

Instalar el script de despliegue y la regla de sudo

Este es el corazón del sistema. Hace tres cosas: git pull como root, devolver a www-data los archivos que trajo el pull y correr una sola vez cada migración pendiente como www-data. Todo va dentro de main() para que bash lea el archivo completo antes de que el pull lo pueda cambiar.

#!/usr/bin/env bash
main() {
  set -euo pipefail
  umask 022
  local WP_ROOT=/var/www/tusitio.com
  local REPO=$WP_ROOT/wp-content
  local STATE=/var/lib/sitio-deploy/applied

  cd "$REPO"
  local before after
  before=$(git rev-parse HEAD)
  git pull --ff-only origin main
  after=$(git rev-parse HEAD)

  # Lo que trajo el pull vuelve a ser de www-data
  if [ "$before" != "$after" ]; then
    local f d
    while IFS= read -r -d '' f; do
      chown www-data:www-data -- "$f"
      d=$(dirname -- "$f")
      while [ "$d" != "." ]; do chown www-data:www-data -- "$d"; d=$(dirname -- "$d"); done
    done < <(git diff --name-only -z --diff-filter=AMR "$before" "$after")
  fi

  # Migraciones pendientes, una sola vez cada una, como www-data
  mkdir -p "$(dirname "$STATE")"; touch "$STATE"
  local m name
  for m in $(ls "$REPO"/ops/migrations/*.sh 2>/dev/null | sort); do
    name=$(basename -- "$m")
    grep -qxF "$name" "$STATE" && continue
    echo "=== migración $name ==="
    sudo -u www-data env HOME=/tmp bash -c 'cd "$1" && bash "$2"' _ "$WP_ROOT" "$m"
    echo "$name" >> "$STATE"
  done
}
main "$@"
exit

Lo instalas fuera del repo, validas la regla de sudo con visudo antes de activarla (un sudoers con errores puede romper sudo) y confirmas que el usuario solo pueda correr ese comando:

install -o root -g root -m 755 sitio-deploy.sh /usr/local/sbin/sitio-deploy
echo 'deploy-github ALL=(root) NOPASSWD: /usr/local/sbin/sitio-deploy ""' > /tmp/deploy.sudoers
visudo -cf /tmp/deploy.sudoers
install -o root -g root -m 440 /tmp/deploy.sudoers /etc/sudoers.d/deploy-github
rm /tmp/deploy.sudoers
sudo -l -U deploy-github

El "" al final de la regla significa «sin argumentos». El último comando debe mostrar una sola línea con el script.

3

Instalar WP-CLI verificando su firma

WP-CLI es la línea de comandos oficial de WordPress. Antes de instalarlo comprobamos su SHA-512 contra el que publica el propio proyecto: si no coincide, no se instala.

curl -fsSL -o /tmp/wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
echo "$(curl -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512)  /tmp/wp-cli.phar" | sha512sum -c
install -o root -g root -m 755 /tmp/wp-cli.phar /usr/local/bin/wp && rm /tmp/wp-cli.phar
sudo -u www-data env HOME=/tmp wp --path=/var/www/tusitio.com core version
4

Crear la llave de GitHub y probar la cadena completa

La llave se autoriza con restrict (sin terminal ni túneles) y un command= que fuerza a ejecutar siempre el script. Luego obtienes la huella del servidor para GitHub y haces una prueba real, entrando por la misma vía que usará Actions.

install -d -m 700 /root/deploy-key
ssh-keygen -t ed25519 -N "" -C "github-actions" -f /root/deploy-key/id_ed25519 -q

install -d -o deploy-github -g deploy-github -m 700 /home/deploy-github/.ssh
printf 'restrict,command="sudo -n /usr/local/sbin/sitio-deploy" %s\n' \
  "$(cat /root/deploy-key/id_ed25519.pub)" > /home/deploy-github/.ssh/authorized_keys
chown deploy-github:deploy-github /home/deploy-github/.ssh/authorized_keys
chmod 600 /home/deploy-github/.ssh/authorized_keys

ssh-keyscan -p 2222 203.0.113.10          # huella para el secret VPS_KNOWN_HOSTS
ssh -i /root/deploy-key/id_ed25519 -p 2222 deploy-github@127.0.0.1 deploy   # prueba real

Si la prueba responde con el git pull y las migraciones, la cadena funciona. La salida se ve así (ejemplo real, sin datos sensibles):

=== git pull ===
Already up to date.
=== migración 001-categorias-blog.sh (como www-data) ===
Success: Term updated.
Success: Term updated.
Success: Created category 1251.
Success: Created category 1252.
Success: Set term.
Success: Added term.
Success: Added term.
Success: Updated post 6262.
=== migraciones aplicadas: 1 ===
5

Cargar los secrets en GitHub y borrar la llave del servidor

En tu repositorio: Settings → Secrets and variables → Actions → New repository secret. Ojo: es la configuración del repositorio, no la de tu cuenta.

Formulario New secret en la configuración de Actions de un repositorio de GitHub
El formulario donde se carga cada secret: nombre y valor. GitHub los guarda cifrados y nunca los vuelve a mostrar.
SecretValor
VPS_HOSTLa IP pública del servidor. La IP, no el dominio: si el dominio pasa por el proxy de Cloudflare, SSH no llega.
VPS_PORTTu puerto SSH
VPS_KNOWN_HOSTSTodas las líneas que imprimió ssh-keyscan
VPS_SSH_KEYLa llave privada completa, con las líneas BEGIN y END

Con los cuatro cargados, la llave privada ya no tiene nada que hacer en el servidor:

shred -u /root/deploy-key/id_ed25519 && rm -f /root/deploy-key/id_ed25519.pub && rmdir /root/deploy-key

Por último, en la pestaña Actions del repo eliges el workflow y le das Run workflow. Si todo está bien, el despliegue pasa en verde en unos segundos.

El código no es lo único que cambia

Desplegar archivos es la mitad del trabajo. En WordPress, muchos cambios viven en la base de datos: categorías, extractos, opciones, menús. Si esos cambios se hacen a mano en el panel, se rompe la idea de tener todo automatizado y registrado.

La solución viene del desarrollo con frameworks como Laravel: migraciones. Cada cambio de base de datos es un script numerado en ops/migrations/, escrito con WP-CLI. El script de despliegue ejecuta cada migración una sola vez y guarda el registro de las aplicadas en el servidor, fuera de la web.

Listado del blog con el filtro Sistemas sin resultados y el post destacado como uncategorized
El problema que resolvió la primera migración: el diseño nuevo usaba cuatro categorías, pero los posts seguían con las antiguas. El filtro quedaba vacío y el destacado aparecía como «uncategorized».

Migración 001: categorías del blog

En vez de entrar al panel a renombrar, crear y reasignar categorías, el cambio quedó en un archivo. Es idempotente: si se ejecutara dos veces, el resultado sería el mismo.

#!/usr/bin/env bash
# 001 — Categorías oficiales del blog. Idempotente.
set -euo pipefail

ensure_cat() {   # ensure_cat <slug> <Nombre> [<categoría antigua a renombrar>]
  local slug=$1 name=$2 legacy=${3:-} id
  id=$(wp term list category --slug="$slug" --field=term_id)
  if [ -z "$id" ] && [ -n "$legacy" ]; then
    id=$(wp term list category --name="$legacy" --field=term_id)
  fi
  if [ -n "$id" ]; then
    wp term update category "$id" --name="$name" --slug="$slug"
  else
    wp term create category "$name" --slug="$slug"
  fi
}

ensure_cat sistemas   Sistemas   "Gestion de Servicios"
ensure_cat hardware   Hardware   "Computadoras"
ensure_cat servidores Servidores
ensure_cat software   Software

wp post term set 6262 category servidores --by=slug
wp post update 6262 --post_excerpt="Serie en 2 partes con video real del proceso completo..."
2
categorías renombradas
los posts las conservan, sin reasignar a mano
2
categorías creadas
Servidores y Software
3
posts ordenados
cada uno en su categoría oficial

Corregir también es más rápido

Con este flujo, un error deja de ser un problema de horas. Ejemplo real: al probar un filtro del blog, una categoría con un solo artículo mostraba la tarjeta estirada a todo el ancho. Basta con mandarle la captura a Claude: identifica la causa (la grilla usaba auto-fit), la cambia a auto-fill, hace el push y GitHub lo despliega. Recargas la página y está corregido. Lo mismo con contenido: una segunda migración puede reescribir extractos sin entrar al panel.

Tarjeta de artículo estirada a todo el ancho por un error de grilla CSS
El error de la tarjeta gigante. Entre la captura y la corrección en vivo pasaron un par de minutos, sin tocar el servidor.
Lo que las migraciones no hacen: no se deshacen solas. Para el código basta un git revert, que también se despliega solo; para la base de datos, la red de seguridad sigue siendo un backup reciente (por ejemplo, WPvivid sincronizado a Google Drive) antes de cualquier migración grande.
¿Claude tiene acceso a tu servidor? clic para ver la respuesta
No. El acceso de Claude termina en el repositorio de GitHub: escribe código y hace push. Quien entra al servidor es GitHub Actions, con una llave que solo puede ejecutar el script de despliegue. La configuración inicial del servidor la haces tú, por SSH, una sola vez.
¿Qué pasa si alguien roba la llave de despliegue? clic para ver la respuesta
Podría ejecutar un despliegue, que es exactamente lo que ya hace GitHub: traer lo que está en el repo. No puede abrir una terminal, crear túneles ni correr otros comandos, porque la llave tiene restrict y un comando forzado. Para hacer daño real necesitaría además acceso de escritura al repositorio.
¿Y si la IA se equivoca y rompe la web? clic para ver la respuesta
Pasa: al montar este mismo blog hubo tres errores visuales. La diferencia es que cada corrección tarda minutos. Además, cada despliegue verifica que la web responda 200: si no, el job falla y lo veo en GitHub. Para volver atrás en el código existe git revert, que también se despliega solo. Y la revisión final en la web sigue siendo tuya antes de dar algo por terminado.
¿Cuánto cuesta? clic para ver la respuesta
Normalmente, nada extra. El plan gratuito de GitHub incluye 2.000 minutos al mes de Actions para repositorios privados, y cada despliegue dura segundos. WP-CLI es gratuito. Lo único de pago es la suscripción a la IA y el VPS, que ya tienes.
¿Por qué no usar FTP o un plugin de despliegue? clic para ver la respuesta
FTP no deja historial, no se puede revertir y transmite credenciales con muchos más permisos. Un plugin de despliegue corre dentro de WordPress, con el mismo usuario que la web: si el sitio se ve comprometido, el despliegue también. Con git cada cambio tiene autor, fecha y motivo, y se puede deshacer.
¿Sirve para otros proyectos, como Laravel? clic para ver la respuesta
Sí. La arquitectura es la misma: un usuario de despliegue, un comando forzado y un script fuera del repo. Lo que cambia es el contenido del script: en Laravel serían composer install, php artisan migrate --force y limpiar cachés, en lugar de WP-CLI.
¿Necesito saber programar para montarlo? clic para ver la respuesta
Necesitas sentirte cómodo en una terminal Linux y entender qué hace cada comando antes de ejecutarlo, sobre todo los que corren como root. No copies nada sin leerlo, incluidos los comandos de este artículo.

Glosario

CI/CD
Integración continua / despliegue continuo. Prácticas para que los cambios de código se prueben y lleguen a producción de forma automática.
Pipeline
La secuencia de pasos automáticos por la que pasa un cambio desde el repositorio hasta el servidor.
GitHub Actions
El servicio de automatización de GitHub. Ejecuta tareas en máquinas temporales cuando ocurre un evento, como un push.
Workflow
Archivo YAML en .github/workflows/ que define cuándo y cómo corre una automatización.
Runner
La máquina temporal de GitHub que ejecuta el workflow y se destruye al terminar.
Secret
Valor cifrado guardado en la configuración del repositorio. El workflow lo usa, pero nadie puede volver a leerlo.
Push
Enviar commits del equipo local al repositorio remoto en GitHub.
Fast-forward (--ff-only)
Actualizar solo si no hay cambios en conflicto. Si el servidor tuviera cambios propios, el pull se detiene en vez de mezclar.
SSH
Protocolo cifrado para conectarse a un servidor remoto.
Llave ed25519
Tipo moderno de llave SSH: corta, rápida y segura. La privada se guarda en secreto, la pública se autoriza en el servidor.
known_hosts
Lista de huellas de servidores conocidos. Evita conectarse a un servidor que se hace pasar por el tuyo.
Comando forzado (command=)
Opción de authorized_keys que obliga a una llave a ejecutar siempre el mismo comando, sin importar lo que pida.
restrict
Opción de authorized_keys que quita terminal, túneles y reenvíos a una llave.
sudoers
Configuración que define qué usuario puede ejecutar qué comando con permisos elevados.
Mínimo privilegio
Principio de seguridad: cada pieza recibe solo los permisos que necesita para su tarea.
www-data
El usuario con el que corren Apache y PHP en Ubuntu. WordPress trabaja con sus permisos.
WP-CLI
La línea de comandos oficial de WordPress: permite gestionar posts, categorías, plugins y ajustes sin entrar al panel.
Migración
Script versionado que aplica un cambio a la base de datos una sola vez.
Idempotente
Una operación que, ejecutada varias veces, deja el mismo resultado que ejecutada una vez.
git revert
Crea un commit nuevo que deshace otro anterior, sin borrar el historial.
Por Alam SilBa · Italo Tecnología · Octubre 2026. Guía construida y verificada en un servidor real con Claude (Anthropic) como copiloto de desarrollo. Los datos de conexión de este artículo son de ejemplo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *