Llegaste al final del Nivel 9. Llevas seis capítulos montando piezas por separado: el flujo de ramas y PRs, el grafo de módulos y el tree-shaking, el pipeline de CI con GitHub Actions, el deploy a Pages, las meta tags de SEO y la capa PWA. Ahora las juntas todas. No hay concepto nuevo; es integración: tu Team Builder de React sale a producción de verdad.
El ciclo completo, de un vistazo#
Así queda el pipeline cuando el proyecto esté terminado:
trabajas en una rama → abres un PR
│
▼
CI: lint + tests + build ← (cap. 3 GitHub Actions)
[merge bloqueado si falla] ← (branch protection)
│
▼
mergeas a main
│
▼
CD: build + deploy a Pages ← (cap. 4 deploy)
[solo si CI pasó]
│
▼
URL pública: Team Builder online ← (cap. 4 base en vite.config.ts)
instalable desde el navegador ← (cap. 6 manifest + SW)
funciona sin conexión ← (cap. 6 caché offline)Cada flecha del diagrama corresponde a algo que ya hiciste en un capítulo del nivel. Aquí los encadenas en un sistema que se ejecuta solo.
Lo que vas a entregar#
La misión es que en tu repositorio de GitHub exista un pipeline de CI/CD que funcione de extremo
a extremo: verificación en cada PR y publicación automática en cada push a main. La app debe ser
una PWA instalable con soporte offline. Y como el proyecto es iterativo, arrastra la calidad
de todos los niveles anteriores: el HTML sigue siendo semántico, el CSS aguanta a 375px en móvil,
no hay any en el TypeScript y los tests están en verde. Acumulas una capa más; no descartas nada.
La URL final, https://tu-usuario.github.io/team-builder/, es el resultado visible. Pero el
aprendizaje de verdad está en el proceso: en la fricción de conectar las piezas, en depurar el
primer 404 por un base mal configurado, en ver el CI fallar por primera vez y bloquear el merge
como debe. Eso es lo que diferencia saber los conceptos de haberlos aplicado.
Cómo encajan las piezas#
Cada capítulo del nivel aporta algo concreto al sistema:
Git avanzado (cap. 1) pone el flujo: cada tarea en su propia rama, el PR como punto de
revisión, el rebase interactivo para limpiar la historia antes del merge. La calidad de la historia
de main —commits en formato convencional, sin mensajes de “wip”— es parte del criterio de
excelente.
Bundling a fondo (cap. 2) explica lo que hay en dist/ después del build: los chunks de JS
con tree-shaking, el CSS, el manifest, el service worker y los iconos. El pipeline no toca esos
ficheros; los arrastra el build. Saber lo que hay ahí te permite depurar cuando algo falta.
CI con GitHub Actions (cap. 3) monta la portería: el job ci ejecuta lint, tests y build en
cada PR. La branch protection bloquea el merge si falla. El ejercicio de este capítulo ya tenías
un ci.yml; aquí lo integras con el deploy en un solo workflow o en dos ficheros separados.
Deploy a Pages (cap. 4) publica el dist/. Las dos cosas críticas: el base en
vite.config.ts con el nombre exacto del repositorio, y GitHub Pages activado en modo “GitHub
Actions” en Settings > Pages. El deploy.yml que montaste en ese capítulo es la base del job
deploy de aquí.
SEO (cap. 5) ya está hecho: las meta tags del <head> —<title>, <meta name="description">,
las etiquetas Open Graph— viven en el index.html. El build las arrastra a dist/ sin que el
pipeline las toque. Los crawlers de redes sociales las leerán del HTML estático sin necesitar
ejecutar JavaScript.
PWA (cap. 6) también está hecho: el manifest.json en public/ y el service worker
con su caché offline. El build los copia a dist/. Cuando el deploy publique ese dist/, la app
será instalable y funcionará sin conexión desde la URL de Pages.
La integración es el trabajo de este capstone. Conecta las piezas, verifica el pipeline de extremo a extremo, y cuando la URL esté viva y el CI funcione como portería, habrás cerrado el Nivel 9: sabes construir, verificar y publicar una aplicación frontend de principio a fin.
Examen del Nivel 9#
18 preguntas que repasan los seis capítulos del nivel: git avanzado, bundling, CI con GitHub Actions, deploy, SEO y PWA. Algunas combinan temas, otras esconden una trampa donde la opción incorrecta parece razonable. Es el examen que cierra el nivel; tómatelo como tal.
Pregunta 1 de 18
¿Cuál es la diferencia clave entre merge y rebase en el resultado de la historia de git?
El proyecto#
Este es un proyecto en local, sobre tu Team Builder de React. No hay playground: un pipeline
de CI/CD necesita un repositorio real, ramas, pull requests y un runner de GitHub Actions para
ejecutarse. El resultado solo es visible cuando la app está publicada en tu URL de Pages. Lee el
README del ejercicio antes de empezar; tiene la lista de comprobación completa y los requisitos
previos. Cuando termines (o si te atascas), despliega las soluciones: los tres ci-cd.yml muestran
el pipeline integrado de cada tier y los comentarios explican por qué cada nivel supera al anterior.
Ejercicio · hazlo en local
Despliega el Team Builder
Lleva tu Team Builder de React a producción de verdad. La misión: que en tu repositorio de GitHub exista un pipeline de CI/CD que verifique el código en cada PR y publique la app en GitHub Pages en cada push a main. La app debe ser una PWA instalable y funcionar offline. No hay concepto nuevo: es integrar todo lo que has montado capítulo a capítulo —el flujo de PR, el CI con GitHub Actions, el deploy, las meta tags de SEO y el service worker— en un sistema que funcione de extremo a extremo. El proyecto es ITERATIVO: no basta con que la URL exista, también deben seguir bien el HTML semántico, el CSS responsive (aguanta a 375px), los tipos sin any y los tests en verde. Acumulas una capa más; no descartas nada de lo anterior.
Paso 1: La URL existe y el pipeline lleva el código a producción
- Existe .github/workflows/ci-cd.yml con un job que construye la app con pnpm build y la publica en GitHub Pages. La URL https://tu-usuario.github.io/team-builder/ responde y muestra el Team Builder.
- vite.config.ts tiene base: '/team-builder/' con el nombre exacto del repositorio. GitHub Pages está activado en modo "GitHub Actions" en Settings > Pages.
- El deploy es automático: un push a main lo dispara sin intervención manual. El dist/ contiene el manifest.json, el service worker y las meta tags del index.html: el build los arrastra.
Paso 2: CI como portería: el código roto no llega a producción
- El workflow tiene dos jobs separados: ci ejecuta lint, tests y build con caché de pnpm; deploy tiene needs: ci y solo corre en push a main. El mismo job ci corre también en los pull requests, donde actúa como status check.
- Branch protection activada en main: el merge requiere que ci pase en verde. Si introduces un test roto o un error de lint, el CI falla, GitHub bloquea el merge y el deploy no se ejecuta. Pruébalo a propósito.
- No hay ningún secreto en una variable VITE_: las VITE_ se incrustan en el bundle y son públicas. Los datos sensibles van en el backend.
Paso 3: El ciclo completo: del commit al deploy, con calidad en todas las capas
- El job ci usa una matrix de Node (22 y 24): lint, tests y build corren en paralelo sobre las dos versiones en cada PR. Ambas deben pasar en verde para que branch protection permita el merge. La URL publicada aparece en la interfaz de Actions al terminar el deploy.
- La app es una PWA offline verificada: Chrome muestra "Instalar", la app arranca con Network > Offline en DevTools, y el dist/ del deploy contiene el manifest, el service worker y las meta tags de Open Graph.
- La historia de main es impecable: commits en formato convencional (feat:, fix:, chore:), sin mensajes de "wip" ni "arreglo". El HTML sigue siendo semántico, el CSS aguanta a 375px, no hay any en TypeScript y los tests están en verde. Este es el ciclo completo: del commit al deploy.
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/proyecto-desplegar-el-team-builder
# abre index.html en el navegador y edita solucion.js Ver soluciones
# Tier ok — el mínimo para estar en producción
#
# Un único job hace todo el trabajo: obtiene el código, instala dependencias,
# construye la app y la publica en GitHub Pages.
#
# La app está online y accesible en tu URL de Pages. Eso ya es un logro real.
#
# Límite de este tier: el job no ejecuta lint ni tests antes de construir.
# Si introduces un error de TypeScript, un test roto o una regla de ESLint violada,
# el workflow lo ignora y publica igualmente. Para que el código roto no llegue a
# producción hace falta separar CI y CD en jobs distintos, como hacen los tiers
# siguientes.
name: CI/CD
# Solo se ejecuta en push a main.
# Los pull requests no disparan este workflow: no hay verificación previa al merge.
on:
push:
branches:
- main
# Permisos que GitHub Pages necesita para publicar desde un workflow.
# contents: read permite al job leer el código del repositorio.
# pages: write permite subir el artifact a GitHub Pages.
# id-token: write es necesario para que deploy-pages se autentique con el entorno.
permissions:
contents: read
pages: write
id-token: write
# concurrency evita que dos deploys corran a la vez sobre el mismo entorno de Pages.
# cancel-in-progress: false deja que el deploy en curso termine antes de empezar otro:
# un deploy a medias puede dejar la app en un estado inconsistente.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Un solo job que construye y despliega sin verificación previa.
build-and-deploy:
runs-on: ubuntu-latest
# environment registra este job como despliegue en el entorno github-pages.
# GitHub muestra un historial de despliegues y protecciones de entorno aquí.
environment:
name: github-pages
# steps.deployment.outputs.page_url lo devuelve actions/deploy-pages
# con la URL final donde la app queda publicada
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Obtener el código
uses: actions/checkout@v6
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Configurar Node.js con caché de pnpm
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
# IMPORTANTE: vite.config.ts debe tener base: '/team-builder/' con el nombre
# exacto de tu repositorio. Sin esa línea, Vite genera rutas absolutas
# (/assets/index-xxxx.js) que el navegador resuelve contra la raíz del dominio,
# no contra el subdirectorio de Pages, y la app carga en blanco con errores 404.
- name: Construir la app
run: pnpm build
# configure-pages prepara el entorno de Pages y calcula la URL base.
# Debe ejecutarse antes de subir el artifact.
- name: Configurar GitHub Pages
uses: actions/configure-pages@v5
# upload-pages-artifact empaqueta el contenido de dist/ como artifact de Pages.
# El dist/ ya contiene el manifest.json, el service worker y las meta
# tags del index.html: el pipeline los arrastra del build, no los toca.
- name: Subir dist/ como artifact de Pages
uses: actions/upload-pages-artifact@v4
with:
path: dist/
# deploy-pages publica el artifact en GitHub Pages y devuelve la URL final.
# id: deployment permite referenciar su output en environment.url más arriba.
- name: Desplegar en GitHub Pages
id: deployment
uses: actions/deploy-pages@v4 Por qué este nivel
- Un único job hace todo el trabajo: checkout, instalar, construir y publicar. La app está en producción y eso ya es real. Su límite está documentado en el propio fichero: sin lint ni tests antes del build, el código roto llega a producción igual.
- El límite que separa ok de mejor: el job no ejecuta lint ni tests. Si introduces un error de TypeScript o un test roto, el workflow lo ignora y publica igualmente. Para que el código roto no llegue a producción hace falta separar CI y CD en jobs distintos.
# Tier mejor — CI y CD separados: el código roto no llega a producción
#
# Supera al tier ok en dos aspectos clave:
#
# 1. Dos jobs separados: ci y deploy.
# El job ci ejecuta lint, tests y build (con caché de pnpm para ir más rápido).
# El job deploy tiene needs: ci, así que solo arranca si ci terminó en verde.
# Si lint falla o un test revienta, deploy no se ejecuta y la app no se publica.
#
# 2. El workflow corre en dos eventos: push y pull_request.
# En los pull requests, solo corre ci (deploy no arranca porque github.ref no es main).
# Ese ci es el status check que configuras en branch protection (Settings > Branches >
# Add branch protection rule > "Require status checks to pass before merging"):
# cuando lo activas, GitHub bloquea el botón "Merge" hasta que ci pase en verde.
# El resultado: ningún commit llega a main sin haber pasado lint y tests.
#
# Diferencia con el tier excelente: ci corre en una única versión de Node (24),
# no en una matrix. El deploy reutiliza el dist/ construido por ci vía artifact
# (evita construir dos veces), pero sin la URL expuesta en la interfaz de Actions.
name: CI/CD
# push a main dispara ci + deploy (si ci pasa).
# pull_request dispara solo ci, que actúa como status check del PR.
on:
push:
branches:
- main
pull_request:
# Permisos que GitHub Pages necesita para publicar.
permissions:
contents: read
pages: write
id-token: write
# cancel-in-progress: false protege los deploys en curso.
# Un deploy de Pages a medias podría dejar la app en estado inconsistente,
# así que dejamos que termine antes de iniciar otro.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Job 1: verificación y construcción.
# Corre en todos los push y en todos los pull requests.
ci:
runs-on: ubuntu-latest
steps:
- name: Obtener el código
uses: actions/checkout@v6
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9
# cache: 'pnpm' le indica a setup-node que guarde node_modules en caché
# usando el lockfile como clave. Si el lockfile no cambia entre runs,
# pnpm install tarda segundos en vez de minutos.
- name: Configurar Node.js con caché de pnpm
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Linting
run: pnpm lint
- name: Ejecutar tests
run: pnpm test
# IMPORTANTE: vite.config.ts debe tener base: '/team-builder/' con el nombre
# exacto de tu repositorio en GitHub. Sin esa línea la app carga en blanco.
- name: Construir la app
run: pnpm build
# Solo subimos el artifact si el job va a desembocar en un deploy.
# En los pull requests este paso se salta: no necesitamos el artifact
# porque deploy no va a correr. La condición usa github.ref para
# detectar si estamos en main o en una rama de PR.
- name: Subir dist/ como artifact de Pages
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v4
with:
path: dist/
# Job 2: publicación en GitHub Pages.
# Solo corre cuando ci pasa Y el push fue a main.
# En pull requests, deploy no se ejecuta aunque ci pase: la condición
# github.ref == 'refs/heads/main' la descarta.
deploy:
# needs: ci significa que este job espera a que ci termine.
# Si ci falla, deploy se cancela automáticamente sin necesidad de condiciones
# adicionales; la condición if añade el segundo requisito: que sea main.
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: github-pages
steps:
- name: Configurar GitHub Pages
uses: actions/configure-pages@v5
# deploy-pages descarga automáticamente el artifact subido por
# upload-pages-artifact en el mismo workflow y lo publica.
# No hace falta checkout ni build: el dist/ ya está verificado por ci.
- name: Desplegar en GitHub Pages
uses: actions/deploy-pages@v4 Por qué es mejor que el anterior
- Dos jobs separados: ci (lint, tests, build con caché de pnpm) y deploy (needs: ci, solo en push a main). El workflow corre en dos eventos: push a main dispara los dos jobs; pull_request dispara solo ci, que actúa como status check para branch protection.
- Lo que supera al tier ok: el código roto ya no llega a producción. Si lint falla o un test revienta, deploy se cancela. Y en los PRs, el merge queda bloqueado hasta que ci pase. Es la portería que separa un pipeline de juguete de uno de empresa.
# Tier excelente — matrix, URL visible y secrets documentados
#
# Supera al tier mejor en tres aspectos:
#
# 1. Matrix de Node en ci: lint, tests y build corren en paralelo sobre Node 22 y
# Node 24 en cada push y en cada pull request. Si una API de Node cambia de
# comportamiento entre versiones lo detectas antes de que llegue a producción.
# En la pestaña Actions aparecen dos jobs simultáneos, uno por versión, cada uno
# con su propio log. Es el mismo status check que configuras en branch protection
# para bloquear el merge de un PR hasta que ambas versiones pasen en verde.
#
# 2. La URL publicada aparece en la interfaz de GitHub Actions al terminar el deploy,
# sin tener que ir a Settings > Pages para encontrarla. La produce el output
# page_url de actions/deploy-pages, expuesto en environment.url.
#
# 3. Documentación de variables de entorno y secrets en el paso de build del deploy:
# qué es público (VITE_), qué nunca puede ir al bundle y cómo proteger datos
# sensibles con un backend intermedio.
#
# El job deploy reconstruye la app sobre el mismo commit que ci verificó.
# Un build de Vite es determinista: dado el mismo código y las mismas dependencias
# produce el mismo bundle. Lo que ci prueba es lo que deploy publica.
# El dist/ que sube upload-pages-artifact ya contiene el manifest.json,
# el service worker (de public/) y las meta tags del index.html (caps. 5 y 6):
# el pipeline los arrastra del build, no los toca ni los genera.
#
# Este es el ciclo completo: del commit al deploy.
# Un push en una rama abre un PR → ci verifica en Node 22 y 24 → branch protection
# bloquea el merge si falla → mergeas a main → deploy reconstruye y publica →
# la URL de Pages muestra tu Team Builder, instalable y offline.
name: CI/CD
# push a main dispara ci (matrix Node 22 y 24) + deploy (si ci pasa).
# pull_request dispara solo ci con la misma matrix: es el status check del PR.
on:
push:
branches:
- main
pull_request:
# Permisos que GitHub Pages necesita para publicar.
permissions:
contents: read
pages: write
id-token: write
# cancel-in-progress: false protege los deploys en curso.
# Un deploy de Pages interrumpido puede dejar la app en estado inconsistente.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Job 1: verificación en matrix de Node 22 y 24.
# Corre en todos los push a main y en todos los pull requests.
# Es verificación pura: lint, tests y build. No sube ningún artifact.
ci:
runs-on: ubuntu-latest
# strategy.matrix define una rejilla de combinaciones que GitHub ejecuta en paralelo.
# Con dos versiones de Node se lanzan dos jobs simultáneos: uno con Node 22 y otro con Node 24.
# Ambos deben pasar en verde para que branch protection permita el merge del PR.
strategy:
matrix:
node-version: [22, 24]
steps:
- name: Obtener el código
uses: actions/checkout@v6
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9
# ${{ matrix.node-version }} inyecta el valor de Node activo en este job.
# En el job con Node 22 vale "22"; en el job con Node 24 vale "24".
# cache: 'pnpm' guarda node_modules con el lockfile como clave.
- name: Configurar Node.js ${{ matrix.node-version }} con caché de pnpm
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Linting
run: pnpm lint
- name: Ejecutar tests
run: pnpm test
- name: Construir la app
run: pnpm build
# Job 2: publica la app en GitHub Pages.
# Solo corre cuando ci terminó en verde Y el push fue a main.
# En pull requests, deploy no se ejecuta: la condición github.ref lo descarta.
deploy:
# needs: ci espera a que todos los jobs de la matrix de ci terminen.
# Si cualquiera de los dos (Node 20 o Node 22) falla, deploy se cancela.
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
# environment con url expone la dirección publicada en la interfaz de Actions.
# Al terminar el job, GitHub muestra un enlace directo a la app desplegada
# en el resumen del workflow, sin necesidad de ir a Settings > Pages.
environment:
name: github-pages
# steps.deployment.outputs.page_url lo devuelve actions/deploy-pages
# con la URL final donde la app queda publicada
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Obtener el código
uses: actions/checkout@v6
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9
# Los jobs corren en máquinas limpias y separadas: deploy no hereda nada de ci.
# Por eso reconstruye la app desde cero con los mismos pasos.
# El build de Vite es determinista: el mismo código produce el mismo bundle.
- name: Configurar Node.js con caché de pnpm
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
# IMPORTANTE: vite.config.ts debe tener base: '/team-builder/' con el nombre
# exacto de tu repositorio en GitHub. Sin esa línea, Vite genera rutas absolutas
# (/assets/index-xxxx.js) que el navegador resuelve contra la raíz del dominio,
# no contra el subdirectorio de Pages, y la app carga en blanco con errores 404.
#
# -----------------------------------------------------------------------
# VARIABLES DE ENTORNO Y SECRETS EN UN DEPLOY ESTÁTICO
# -----------------------------------------------------------------------
# Las variables VITE_ se incrustan en el bundle durante el build.
# Son cadenas de texto dentro de un fichero .js que cualquiera puede leer:
# la URL de una API pública, el nombre del proyecto, una clave de analytics
# sin privilegios. Son adecuadas para configuración no sensible.
#
# NUNCA pongas en una variable VITE_ un secret de verdad:
# una clave privada de API, un token de acceso, una contraseña de base de datos,
# un secreto de firma de JWT. En un deploy estático no hay servidor donde
# esconderlo: el secret termina en el bundle, en el navegador del usuario y,
# por tanto, en manos de cualquiera que abra las DevTools.
#
# Si tu app necesita operar con datos sensibles, la arquitectura correcta es:
# el cliente llama a un endpoint de tu propio backend, que sí tiene entorno
# de servidor seguro (variables de entorno reales, no incrustadas en el bundle),
# y ese backend es el único que usa el secret para llamar a la API externa.
# El cliente nunca ve la clave.
#
# Para pasar una variable VITE_ en el workflow, declárala como secret en
# Settings > Secrets and variables > Actions y úsala así en el paso de build:
# env:
# VITE_API_URL: ${{ secrets.VITE_API_URL }}
# -----------------------------------------------------------------------
- name: Construir la app
run: pnpm build
- name: Configurar GitHub Pages
uses: actions/configure-pages@v5
# upload-pages-artifact empaqueta el contenido de dist/ como artifact de Pages.
# El dist/ ya contiene el manifest.json, el service worker y las meta
# tags del index.html: el build los arrastra, el pipeline no los toca.
- name: Subir dist/ como artifact de Pages
uses: actions/upload-pages-artifact@v4
with:
path: dist/
# deploy-pages publica el artifact en GitHub Pages y devuelve la URL final.
# id: deployment permite referenciar su output page_url en environment.url.
- name: Desplegar en GitHub Pages
id: deployment
uses: actions/deploy-pages@v4 Por qué es mejor que el anterior
- El job ci usa una matrix de Node 22 y 24: en cada PR se lanzan dos jobs en paralelo, uno por versión. Ambos deben pasar en verde para que branch protection permita el merge. La URL publicada aparece en la interfaz de Actions gracias al output page_url de deploy-pages expuesto en environment.url.
- El paso de build del job deploy documenta en detalle la política de secrets: qué variables VITE_ son adecuadas (configuración pública), qué nunca puede ir ahí (claves privadas, tokens de acceso) y cómo proteger datos sensibles con un backend intermedio. Este es el ciclo completo: del commit al deploy.