learning-front

Nivel 9 · El ciclo completo: del commit al deploy

Deploy: Vercel, GitHub Pages y secrets

Publicar tu web de verdad en internet: qué es un hosting estático, desplegar a GitHub Pages con Actions (y la trampa del base de Vite) o a Vercel sin configuración, los preview deploys por PR, y cómo se gestionan las variables de entorno y los secrets en producción.

En el capítulo 2 de este nivel construiste el dist/: el bundle de producción comprimido, con hashes y listo para servirse. En el capítulo 3 montaste el pipeline de CI que verifica cada push. En ese pipeline dejaste un job deploy: con needs: ci y un echo "publicando...". Ese hueco es exactamente lo que rellenas aquí.

Publicar en internet es el paso que hace que todo lo anterior importe. Un build que nadie puede visitar no es un producto; es un fichero en una carpeta de tu portátil.

Desplegar una SPA es servir ficheros estáticos#

Cuando ejecutas pnpm build, Vite produce una carpeta dist/ con ficheros estáticos: index.html, uno o varios .js, uno o varios .css y los assets (imágenes, fuentes). No hay ningún proceso Node esperando peticiones. No hay lógica de servidor. Son ficheros.

Desplegar esa carpeta significa ponerla en un servidor web que los sirva por HTTP. Cualquier navegador que pida https://tu-dominio.com/ recibirá el index.html y, a partir de ahí, el navegador carga el JS y React toma el control.

Eso tiene una consecuencia importante: una SPA se puede hospedar de forma gratuita en muchas plataformas porque no necesitan ejecutar código por ti. Solo almacenan ficheros y los sirven. GitHub Pages, Vercel, Netlify y decenas de alternativas ofrecen esto sin coste para proyectos personales o de aprendizaje.

La diferencia con lo que verás en el Nivel 10 (backend con Express) es exactamente esta: un servidor de aplicación sí ejecuta código por cada petición. Una SPA, no.

GitHub Pages con Actions#

GitHub Pages es el hosting estático de GitHub. Está integrado en el repositorio: no hace falta crear ninguna cuenta nueva ni configurar ningún servicio externo. Para activarlo basta con ir a Settings → Pages en tu repositorio y elegir “GitHub Actions” como fuente.

El flujo de publicación tiene cuatro pasos que siempre van en este orden:

yaml
# Paso 1: prepara el entorno de Pages en el runner
- name: Configurar GitHub Pages
  uses: actions/configure-pages@v5

# Paso 2: empaqueta el contenido de dist/ como artifact de Pages
# path: dist/ indica la carpeta que Vite genera al construir
- name: Subir artifact de Pages
  uses: actions/upload-pages-artifact@v4
  with:
    path: dist/

# Paso 3: despliega el artifact en GitHub Pages y devuelve la URL final
- name: Desplegar en GitHub Pages
  uses: actions/deploy-pages@v4

Para que el workflow pueda publicar, necesita tres permisos que van al nivel del workflow, fuera de los jobs:

yaml
# Estos permisos son obligatorios; sin alguno de los tres, el deploy falla con 403
permissions:
  # Leer el código del repositorio
  contents: read
  # Subir el artifact al almacenamiento de Pages
  pages: write
  # Obtener el token de autenticación de Pages (OIDC)
  id-token: write

Y el job de deploy debe declarar que trabaja en el entorno de GitHub Pages:

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    # environment registra este deploy en la pestaña Environments del repositorio
    environment: github-pages
    steps:
      # ... los cuatro pasos de arriba

La trampa del base#

Cuando publicas en Pages, tu app vive en un subdirectorio:

texto
https://tu-usuario.github.io/team-builder/

No en la raíz del dominio. Vite, sin configuración adicional, genera rutas absolutas para los assets:

texto
/assets/index-a1b2c3d4.js
/assets/styles-e5f6g7h8.css

El navegador resuelve esas rutas contra la raíz del dominio y pide:

texto
https://tu-usuario.github.io/assets/index-a1b2c3d4.js

Ese fichero no existe en esa ruta. El resultado es silencioso: la página carga, aparece en blanco y en la consola de red hay 404. Es el fallo número uno al desplegar en Pages, y no hay ningún mensaje de error que lo explique.

La solución es configurar base en vite.config.ts antes de hacer el primer deploy:

typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  // base debe coincidir exactamente con el nombre de tu repositorio en GitHub
  base: '/team-builder/',
  plugins: [react()],
});

Con base configurado, Vite genera rutas relativas al subdirectorio:

texto
/team-builder/assets/index-a1b2c3d4.js

y el navegador las encuentra. Haz este cambio en tu proyecto antes de copiar el workflow: si no, el workflow desplegará una app rota.

Vercel: cero configuración#

Vercel es una plataforma de hosting que detecta el tipo de proyecto automáticamente. Para desplegar el Team Builder en Vercel no necesitas escribir ningún workflow: conectas el repositorio desde el panel web de Vercel y cada push a main desencadena el build y el deploy sin más configuración.

La diferencia clave con Pages es que Vercel sirve desde la raíz de un dominio propio:

texto
https://team-builder.vercel.app/

No hay subdirectorio, así que el problema del base no existe. Tampoco hay que activar permisos ni escribir pasos de YAML.

El trade-off es claro. Pages te da control total: el workflow es tuyo, está versionado en el repositorio, el equipo puede auditarlo y modificarlo. Cualquier ingeniería puede leer qué pasa exactamente con cada push. Vercel te da comodidad: conectas el repo y funciona, pero confías en que la plataforma hace lo correcto por ti. Ambas opciones son válidas y están en uso en proyectos profesionales reales.

Preview deploys: una URL por cada PR#

En el capítulo 1 de este nivel abriste Pull Requests con gh pr create. Un PR representa un cambio pendiente de revisión. Revisar un PR mirando solo el diff del código tiene un límite: no ves cómo se comporta la UI en el navegador.

Un preview deploy resuelve esto: cuando alguien abre un PR, la plataforma construye la app con ese código y la publica en una URL temporal con el número del PR:

texto
https://team-builder-pr-42.vercel.app/

El revisor abre esa URL, usa la app real, navega, interactúa, y detecta problemas que no se ven en el código. Cuando el PR se fusiona a main, el preview se destruye. Si hay varios PRs abiertos, cada uno tiene su propia URL.

En Vercel esto funciona sin configuración adicional: cada PR genera su preview automáticamente. En GitHub Pages no hay soporte nativo para preview deploys: conseguirlo requiere scripts adicionales que publican en subdirectorios temporales o en repositorios separados. Es posible pero complejo. Si los preview deploys son importantes en tu flujo de trabajo, Vercel es la opción más directa.

Variables de entorno y secrets en producción#

En el Nivel 4 aprendiste la regla: una variable que empieza por VITE_ se incrusta en el bundle de producción. Es texto plano dentro de un fichero .js que el navegador descarga. Cualquiera que abra DevTools puede leerla.

Esa regla no cambia en producción. La aplicarla bien aquí tiene consecuencias económicas y de seguridad reales.

Lo que sí puede ir en una variable VITE_: configuración no sensible que el cliente necesita conocer. La URL de una API pública, el nombre de tu proyecto para analytics, una clave de un servicio de mapas sin privilegios de escritura.

yaml
# En el paso de build del workflow, la variable se inyecta desde el secret del repositorio
- name: Construir la app
  run: pnpm build
  env:
    # VITE_API_URL acabará en el bundle: asegúrate de que no es un dato sensible
    VITE_API_URL: ${{ secrets.VITE_API_URL }}

Lo que nunca puede ir en una variable VITE_: claves privadas de API con privilegios de escritura, tokens de acceso, contraseñas de bases de datos, secretos de firma de JWT. En un deploy estático no hay servidor donde esconderlos: acabarían en el bundle, en el navegador del usuario y, por tanto, en manos de cualquiera que abra las DevTools.

Si el Team Builder necesita operar con datos sensibles, la arquitectura correcta es:

texto
navegador (React) → tu backend (Express, Nivel 10) → API externa

El cliente llama a un endpoint propio de tu backend. El backend guarda el secret en sus variables de entorno de servidor (que nunca llegan al cliente) y llama a la API externa. El cliente nunca ve la clave.

El secret del repositorio (Settings → Secrets and variables → Actions) es donde guardas los valores que el workflow necesita en tiempo de ejecución. GitHub los cifra, los enmascara en los logs y nunca los expone. Pero recuerda: si los inyectas como VITE_, siguen acabando en el bundle.

Conectar el deploy al CI/CD#

El capítulo 3 dejó este esqueleto en el workflow:

yaml
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - run: pnpm test
      - run: pnpm build

  deploy:
    # needs: ci hace que deploy espere a que ci pase en verde
    needs: ci
    # Solo se despliega en pushes a main, nunca en PRs ni en otras ramas
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - run: echo "publicando..."

Ahora rellenas ese echo "publicando..." con los pasos reales de GitHub Pages. El resultado es un pipeline completo:

yaml
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm test
      - run: pnpm build

  deploy:
    needs: ci
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: github-pages
    steps:
      # Cada job parte de una máquina nueva y limpia: el checkout es necesario aquí también
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      # Se construye de nuevo porque el dist/ del job ci no persiste entre jobs
      - run: pnpm build
      - uses: actions/configure-pages@v5
      - uses: actions/upload-pages-artifact@v4
        with:
          path: dist/
      - uses: actions/deploy-pages@v4

El status check que bloquea el merge de los PRs (que configuraste con Branch protection rules en el capítulo 1) es el job ci. El job deploy solo corre cuando ese check pasa y el push llega a main. El código roto nunca llega a producción.

El CDN y tu dominio#

Cuando Pages o Vercel sirven tu app, no lo hacen desde un único servidor. Lo hacen desde una red de distribución de contenido (CDN): decenas o cientos de servidores repartidos por todo el mundo que tienen una copia de tus ficheros. Cuando alguien en Tokio abre tu Team Builder, recibe los ficheros desde el servidor más cercano a Tokio, no desde un centro de datos en Estados Unidos. La latencia se reduce de cientos de milisegundos a decenas.

Los assets con hash que genera Vite (como index-a1b2c3d4.js) son perfectos para el CDN. El CDN puede configurarlos con una caducidad de caché muy larga: si el nombre no cambia, el contenido no cambia, y el servidor puede servirlo sin consultar el origen. Cuando publicas una nueva versión, el hash cambia, el CDN ve un nombre nuevo, lo descarga del origen y lo sirve fresco a todos los usuarios.

Sin hashes, el CDN serviría un fichero llamado index.js sin saber si es el del deploy de hoy o el de hace seis meses. Con hashes, tiene certeza. Por eso el capítulo 2 explicaba que los hashes son una decisión de arquitectura, no un detalle cosmético.

Si quieres conectar un dominio propio (teambuilder.tudominio.com) a Pages o a Vercel, basta con ir al panel de la plataforma, añadir el dominio y apuntar el registro DNS donde compres el dominio al servidor que te indique la plataforma. Los DNS los viste en el Nivel 0: el registro CNAME o A apunta tu dominio al host. La plataforma se encarga del resto, incluido el certificado HTTPS.

Cuando un deploy a main rompe producción, la forma más rápida de volver a un estado bueno es revertir el commit problemático con git revert —el comando seguro que viste en el capítulo 1: crea un commit nuevo que deshace los cambios de otro, sin reescribir la historia, así que vale en una rama compartida como main— y dejar que el pipeline redepliegue la versión anterior de forma automática. GitHub Pages y Vercel también conservan el historial de deploys en su panel, donde puedes restaurar manualmente cualquier deploy anterior sin tocar el código.

Comprueba lo que sabes#

Pregunta 1 de 7

¿Qué significa desplegar una SPA?

Tu turno#

El ejercicio se hace en local, sobre el Team Builder de React que vienes construyendo desde el Nivel 7. Necesitas el repositorio subido a GitHub y que pnpm lint, pnpm test y pnpm build pasen en verde en tu máquina antes de empezar.

Ejercicio · hazlo en local

Deploy del Team Builder en GitHub Pages

Sobre el repositorio de tu Team Builder de React (el que vienes construyendo desde el Nivel 7), crea el fichero `.github/workflows/deploy.yml` y verifica que la app se publica en GitHub Pages tras un push a main. Las soluciones muestran el workflow completo de cada tier con comentarios que explican cada decisión. Haz el tuyo primero y luego compara.

Paso 1: Deploy mínimo funcional

  • Añades `base: '/nombre-del-repo/'` en `vite.config.ts` (con el nombre exacto de tu repositorio) antes de crear el workflow.
  • Activas GitHub Pages en modo "GitHub Actions" en Settings > Pages del repositorio.
  • Creas `.github/workflows/deploy.yml` con los permisos correctos (`contents: read`, `pages: write`, `id-token: write`), el entorno `github-pages` y los pasos: checkout → instalar pnpm → configurar Node → instalar dependencias → build → configure-pages → upload-pages-artifact → deploy-pages.
  • Haces commit y push a main; en la pestaña Actions ves el workflow ejecutarse en verde.
  • Entras en la URL publicada (`https://tu-usuario.github.io/nombre-del-repo/`) y el Team Builder aparece y funciona.

Cómo hacerlo en local

Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el index.html en tu navegador. Toda tu solución va en solucion.js.

git clone <repo>
cd exercises/nivel-9/deploy
# abre index.html en el navegador y edita solucion.js
Ver soluciones
# Tier ok — deploy mínimo a GitHub Pages
# Un solo job construye la app con pnpm y la publica en GitHub Pages.
# La app queda online tras cada push a main.
# Límite de este tier: no corre tests ni lint antes de desplegar.
# Puedes publicar código roto sin que nadie lo impida.

name: Deploy

on:
  push:
    branches:
      # Solo se dispara cuando el push va a main, no en ramas de trabajo
      - main

# Permisos que el workflow necesita para publicar en GitHub Pages.
# Sin estos tres permisos, actions/deploy-pages falla con 403.
# contents: read — leer el código del repositorio
# pages: write — subir el artifact al almacenamiento de Pages
# id-token: write — obtener el token de autenticación de Pages (OIDC)
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest

    # environment le dice a GitHub que este job publica en el entorno "github-pages".
    # GitHub registra el deploy en la pestaña Environments del repositorio.
    environment: github-pages

    steps:
      # Paso 1: descarga el código del repositorio en la máquina virtual
      - name: Obtener el código
        uses: actions/checkout@v6

      # Paso 2: instala pnpm antes de usarlo
      - name: Instalar pnpm
        uses: pnpm/action-setup@v4
        with:
          # Versión de pnpm; debe coincidir con la de tu proyecto
          version: 9

      # Paso 3: instala Node.js
      - name: Configurar Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          # Activa la caché de dependencias de pnpm para acelerar las siguientes ejecuciones
          cache: 'pnpm'

      # Paso 4: instala las dependencias exactas del lockfile
      - name: Instalar dependencias
        run: pnpm install --frozen-lockfile

      # Paso 5: construye la app para producción
      # IMPORTANTE: antes de llegar aquí, asegúrate de que vite.config.ts tiene
      # base: '/team-builder/' (con el nombre exacto de tu repositorio).
      # Sin esa configuración, Vite genera rutas absolutas (/assets/...) que el
      # navegador resuelve contra la raíz del dominio en vez de contra el subdirectorio,
      # y la app se carga en blanco con 404 en la consola de red.
      - name: Construir la app
        run: pnpm build

      # Paso 6: prepara el entorno de Pages en la máquina virtual
      # Configura el runner para que pueda interactuar con GitHub Pages
      - name: Configurar GitHub Pages
        uses: actions/configure-pages@v5

      # Paso 7: empaqueta el contenido de dist/ como artifact de Pages
      # path: dist/ indica la carpeta que Vite genera al construir
      - name: Subir artifact de Pages
        uses: actions/upload-pages-artifact@v4
        with:
          path: dist/

      # Paso 8: despliega el artifact en GitHub Pages
      # Este paso publica el contenido y devuelve la URL final en page_url
      - name: Desplegar en GitHub Pages
        uses: actions/deploy-pages@v4

Por qué este nivel

  • El deploy mínimo que cualquier proyecto debería tener: cada push a main construye la app y la publica. Con los permisos correctos, el entorno declarado y los cuatro pasos de Pages (configure → upload → deploy), la app está en internet sin intervención manual.
  • Su límite es que despliega sin pasar tests ni lint: una regresión o un build roto llegan directamente a producción sin que nadie lo impida. El tier "mejor" cierra ese hueco separando el CI del deploy.