Flujo de deploy (staging → prod)
Desde 2026-06-04 hay un pipeline de deploy con dos ambientes. La regla de oro:
Staging se despliega solo. Producción NUNCA se despliega sola. Web y API van a prod con un botón manual cada uno, y tú eliges el orden.
push a `staging` ──► STAGING (automático) · API: GitHub Actions → tcgcards-api-staging · Web: Vercel → staging.tcgcards.cl (basic auth)
↓ pruebas todo en staging ✓
merge la RAMA del feature →`main` (NO despliega nada · NUNCA `staging`→`main`)
↓ pulsas los botones, en el orden que quieras
Actions "Deploy API → PROD" ──► tcgcards-api (Cloud Run) Actions "Deploy Web → PROD" ──► tcgcards.cl (Vercel)
↓ con prod ya funcionando
CIERRE: merge `main` → `staging` + borrar la rama del feature → las dos ramas quedan con el mismo código, listas para la siguienteModelo de ramas (la regla de oro)
Sección titulada «Modelo de ramas (la regla de oro)»main es la fuente de verdad de prod. staging es solo una rama de despliegue = main + lo que se esté probando ahora. Las cuatro reglas:
- Los features salen de
mainy se integran amain(vía PR). Integrar amaines seguro porquemainno se despliega solo. - Para probar en staging: la rama del feature →
staging(git merge feat/...sobrestaging+ push) → staging se despliega. - NUNCA
staging → main. - Al terminar,
main→staging. Con el feature ya en prod y funcionando, ese merge deja las dos ramas con el mismo código y el ciclo siguiente arranca desde un estado conocido. Ver Cerrar el ciclo.
La 3 y la 4 no se contradicen: staging → main mandaría a la fuente de verdad algo a medio probar; main → staging solo pone al día el banco de pruebas con lo que ya pasó por prod.
El orden en la práctica (más seguro): primero feat → staging y pruebas en staging; recién cuando funciona, feat → main para prod. Probar antes de integrar a main evita meter a la fuente de verdad algo no validado.
Herramientas de prueba que sí están en main (y por qué es seguro)
Sección titulada «Herramientas de prueba que sí están en main (y por qué es seguro)»| Pieza | Repo | Dónde vive | Por qué no llega al usuario |
|---|---|---|---|
test-login (entrar como usuario ficticio sin Google) | web | main, desde el PR #43 | Gateado fail-closed: el provider ni se registra si VERCEL_ENV === 'production'. Subió a main porque estar solo en staging lo dejaba fuera del CI y de las revisiones — más riesgoso que tenerlo auditado. Ver la nota sobre el alcance de las variables de Vercel: una variable de Preview sin rama atada la reciben todas las ramas. |
seam MP_FAKE_PAYMENTS (pagos sintéticos para la suite E2E) | api | main, inerte | Fail-closed doble: en prod el flag está apagado y, si se prendiera, el server no arranca (exige no-prod + base con “staging” en el nombre). |
Pasar a STAGING (automático)
Sección titulada «Pasar a STAGING (automático)»Cuándo: cada vez que quieres probar un cambio en un ambiente real sin tocar prod.
- Trabajas en una rama de feature (o directo en
staging). - Llevas los cambios a la rama
stagingdel repo correspondiente:Ventana de terminal git checkout staginggit merge tu-rama # o trabajas directo sobre staginggit push origin staging - El push dispara el deploy a staging automáticamente:
- API (
tcgcards-api): GitHub Actions → workflow “Deploy API → STAGING” → ejecuta CI (lint+test+build) → compila y sube la imagen a Artifact Registry →gcloud run deploy tcgcards-api-staging. - Web (
tcgcards-web): Vercel (nativo) detecta el push astaging→ build → publica enstaging.tcgcards.cl.
- API (
Cómo verificar:
- API: GitHub →
tcgcards-api→ Actions → el run de “Deploy API → STAGING” en verde. Y:Ventana de terminal curl -s -o /dev/null -w "%{http_code}\n" \https://tcgcards-api-staging-1033181994095.us-central1.run.app/api/v1/health# Esperado: 200 - Web: abrir
https://staging.tcgcards.cl→ pide basic auth (usuariostaging+ la clave configurada enSTAGING_BASIC_AUTH). Adentro, navegas como en prod pero contra la DB/credenciales de staging.
Pasar a PROD (manual, con tu OK)
Sección titulada «Pasar a PROD (manual, con tu OK)»Cuándo: ya probaste en staging y quieres llevar el cambio a los usuarios.
1. Promover el código a main
Sección titulada «1. Promover el código a main»Fusionas la rama del feature (NO staging) en main:
git checkout maingit merge feat/tu-feature # ⬅ la RAMA del cambio, NUNCA `git merge staging`git push origin main2. Pulsar los botones — tú eliges el orden
Sección titulada «2. Pulsar los botones — tú eliges el orden»A veces conviene la API primero (si el cambio web depende de un endpoint nuevo), a veces la web primero. Son independientes:
- API → PROD: GitHub →
tcgcards-api→ pestaña Actions → “Deploy API → PROD (manual)” → Run workflow → branchmain→ Run. - Web → PROD: GitHub →
tcgcards-web→ pestaña Actions → “Deploy Web → PROD (manual)” → Run workflow → branchmain→ Run.
Cada botón ejecuta CI primero; si lint/test/build fallan, no despliega.
Cómo verificar:
- API:
Ventana de terminal gcloud run revisions list --service tcgcards-api \--region us-central1 --project api-cards-prod --limit 1 \--format="value(metadata.name, metadata.creationTimestamp)"# Debe aparecer una revisión nueva (timestamp de recién).curl -s -o /dev/null -w "%{http_code}\n" \https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/health# Esperado: 200 - Web: abrir
https://tcgcards.clen incógnito → ver el cambio. En el log del run,vercel deployimprimeAliased https://tcgcards.cl.
Cerrar el ciclo (el paso que mantiene todo ordenado)
Sección titulada «Cerrar el ciclo (el paso que mantiene todo ordenado)»Cuándo: el feature ya está en prod y lo confirmaste funcionando. No antes — si el deploy
sale mal, staging es tu punto de comparación y sincronizarla te lo quita justo cuando lo
necesitas.
git checkout staginggit merge --no-ff maingit push origin staging
# La comprobación que cierra: tiene que salir VACÍOgit diff --stat origin/main origin/stagingY borrar la rama del feature (desde 2026-08-18 GitHub la borra sola al fusionar el PR).
Por qué hace falta
Sección titulada «Por qué hace falta»Al cerrar un feature, las correcciones que salen de la revisión del PR entran solo a main.
staging se queda con la versión anterior y va quedando atrás con cada feature. Es la
dirección inofensiva de la divergencia —no se pierde nada— pero se acumula: el 2026-08-14 a
staging del web le faltaban 5 archivos que main sí tenía.
Compara por archivos, no por commits
Sección titulada «Compara por archivos, no por commits»La única condición para hacerlo
Sección titulada «La única condición para hacerlo»Que en staging no quede otra cosa probándose. Si la hubiera, este merge le aplicaría los
reverts que traiga main. Por eso el paso va después de confirmar prod, cuando lo que se
estaba probando ya salió.
Las barandas (para no depender de la memoria)
Sección titulada «Las barandas (para no depender de la memoria)»Configurado el 2026-08-18 en los dos repos:
| Baranda | Qué hace | Dónde |
|---|---|---|
main protegida | Solo se entra por PR y con el check test en verde. Cero aprobaciones de terceros a propósito: con un solo desarrollador, exigirlas te bloquea a ti mismo. La puerta es el CI, no un revisor. Prohíbe además empujes forzados y borrar main. | Settings → Branches |
| Borrado automático de ramas | Al fusionar el PR, GitHub borra la rama. Es lo que evita volver a acumular 100 ramas. | Settings → General |
Limpiar ramas viejas: comprueba por CONTENIDO, no por «merged»
Sección titulada «Limpiar ramas viejas: comprueba por CONTENIDO, no por «merged»»Una rama solo es segura de borrar si cumple las tres:
git rev-list --count origin/main..origin/<rama> # 0 commits propiosgit diff --diff-filter=A --name-only origin/main origin/<rama> # 0 archivos propiosgit merge-base --is-ancestor origin/<rama> origin/main # sus commits siguen alcanzables desde mainLa tercera es la que hace que borrar no pierda nada: si la punta es ancestro de main, los
commits siguen vivos y para revivir la rama basta git push origin <sha>:refs/heads/<rama>.
Las que NUNCA se borran (guardan trabajo aparcado que no está en ningún otro sitio):
feat/deck-builder, feat/deck-builder-web, feat/marketing-email-ses,
feat/marketing-email-ses-web.
Promoción selectiva (algo solo en staging, o mandar UNA sola cosa a prod)
Sección titulada «Promoción selectiva (algo solo en staging, o mandar UNA sola cosa a prod)»¿Quieres probar un cambio en staging pero nunca mandarlo a prod, mientras sigues mandando otras cosas a prod? Sí se puede. La clave:
A prod llega SOLO lo que integras a
main. Lo que esté enstagingpero nunca se integre amain, nunca llega a prod — aunque viva en staging para siempre.
Por eso, la forma robusta de promover es fusionar la rama del cambio en main, no la
rama staging entera. (Fusionar staging→main solo es seguro cuando todo lo que hay
en staging va a prod.)
Ejemplo — hoy pruebo A (solo staging); mañana pruebo B y mando solo B a prod:
# HOY — A a staging, sin prodgit checkout staging && git merge feat/A && git push origin staging # staging despliega A; pruebas# (no ejecutas el botón de prod → A queda en staging y en feat/A; prod sigue sin A)
# MAÑANA — B a staging y solo B a prodgit checkout staging && git merge feat/B && git push origin staging # staging = A + B; pruebas Bgit checkout main && git merge feat/B && git push origin main # ⬅ la rama de B, NO "merge staging"# → botón "Deploy → PROD". Solo B va a prod; A nunca (nunca fusionaste feat/A en main).Dos aclaraciones:
- El entorno de staging corre UNA sola versión (el estado de la rama
staging). “Dejar A en staging” = A está ahí hasta que despliegues otra cosa; no es que A y B convivan como dos deploys separados. staginges desechable. Si se llena de experimentos que nunca van a prod y la quieres limpiar (o probar B aislado, sin A encima), reséteala a prod:Ventana de terminal git checkout staging && git reset --hard main && git merge feat/B && git push --force origin staging
Rollback (si un deploy de prod sale mal)
Sección titulada «Rollback (si un deploy de prod sale mal)»Web (Vercel): Vercel → tcgcards-web → Deployments → el deploy anterior bueno → “Promote to Production” (instantáneo, sin rebuild).
API (Cloud Run): apuntar el tráfico a la revisión anterior:
gcloud run revisions list --service tcgcards-api \ --region us-central1 --project api-cards-prod --limit 5gcloud run services update-traffic tcgcards-api \ --to-revisions=<REVISION_ANTERIOR>=100 \ --region us-central1 --project api-cards-prodCrear/actualizar un Cloud Run Job + Scheduler
Sección titulada «Crear/actualizar un Cloud Run Job + Scheduler»Crear un Cloud Run Job sigue siendo manual: el pipeline no lo hace. Este es el procedimiento
tal como se montó auto-price el 2026-06-30.
1. Sacar la imagen de prod del servicio
Sección titulada «1. Sacar la imagen de prod del servicio»gcloud run services describe tcgcards-api \ --region us-central1 --project api-cards-prod \ --format="value(spec.template.spec.containers[0].image)"# → ...docker.pkg.dev/.../tcgcards-api@sha256:<SHA> (cópiala como <IMG>)2. Crear (o actualizar) el Job
Sección titulada «2. Crear (o actualizar) el Job»gcloud run jobs create <NOMBRE> \ --image="<IMG>" \ --command=node \ --args="dist/jobs/<X>.js" \ --region us-central1 --project api-cards-prod \ --service-account=1033181994095-compute@developer.gserviceaccount.com \ --memory=512Mi --cpu=1 \ --task-timeout=10800 --max-retries=0 \ --set-secrets="MONGODB_URI=mongodb-uri:latest,ADMIN_TOKEN=admin-token:latest" \ --set-env-vars="..." # mismas env (placeholders) que refresh-prices3. Darle permiso para que el Scheduler lo invoque
Sección titulada «3. Darle permiso para que el Scheduler lo invoque»Sin esto, el Scheduler falla con PERMISSION_DENIED:
gcloud run jobs add-iam-policy-binding <NOMBRE> \ --region us-central1 --project api-cards-prod \ --member="serviceAccount:1033181994095-compute@developer.gserviceaccount.com" \ --role="roles/run.invoker"4. Crear el Scheduler (cron) que dispara el Job
Sección titulada «4. Crear el Scheduler (cron) que dispara el Job»gcloud scheduler jobs create http <NOMBRE>-daily \ --location us-central1 --project api-cards-prod \ --schedule="..." --time-zone="America/Santiago" \ --uri="https://us-central1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/api-cards-prod/jobs/<NOMBRE>:run" \ --http-method=POST \ --oauth-service-account-email="1033181994095-compute@developer.gserviceaccount.com"5. Probar que dispara una ejecución
Sección titulada «5. Probar que dispara una ejecución»gcloud scheduler jobs run <NOMBRE>-daily --location us-central1 --project api-cards-prodgcloud run jobs executions list --job <NOMBRE> \ --region us-central1 --project api-cards-prod --limit 1# Debe aparecer una ejecución nueva (timestamp de recién).Troubleshooting (lecciones del montaje)
Sección titulada «Troubleshooting (lecciones del montaje)»- El botón falla en el job
ci: el deploy gatea en CI. Si lint/test/build están rojos, arréglalo y solo entonces despliega. (No es que “el deploy falló”: no llegó a desplegar a propósito.) - Web prod compila en la infra de Vercel (
vercel deploy --prod, sin--prebuilt). NO se compila local en CI porquenext buildnecesita las System env vars de Vercel (ej.VERCEL_URL) y falla conERR_INVALID_URL. - API usa
--imageen CI (build+push de la imagen a Artifact Registry, luegogcloud run deploy --image), no--source. Así el deployer no necesita Cloud Build ni permisos de storage de proyecto. (En local,scripts/deploy-staging.sh/deploy-prod.shcaen a--sourcesi no hayIMAGE.) react-hooks/refs/ React Compiler: algunos errores de lint son falsos positivos (pasar un objeto ref a un hijo, leer una prop no-ref de un hook). Se suprimen con// eslint-disable-next-line react-hooks/refs+ justificación, no se debilita el lint global.
Referencia rápida
Sección titulada «Referencia rápida»| Pieza | Valor |
|---|---|
| Web prod | tcgcards.cl (Vercel, branch main, auto-deploy apagado → solo botón) |
| Web staging | staging.tcgcards.cl (Vercel, branch staging, basic auth + noindex) |
| API prod | tcgcards-api (Cloud Run) · https://tcgcards-api-1033181994095.us-central1.run.app |
| API staging | tcgcards-api-staging (Cloud Run) · https://tcgcards-api-staging-1033181994095.us-central1.run.app |
| Auth GitHub→GCP | Workload Identity Federation (sin keys), atado al repo + environment |
Variables/secrets de repo (ya configurados):
tcgcards-api(Variables):GCP_WIF_PROVIDER,GCP_DEPLOYER_SA,STAGING_MP_MODE,STAGING_ADMIN_EMAIL.tcgcards-web(Secret):VERCEL_TOKEN. (Variables):VERCEL_ORG_ID,VERCEL_PROJECT_ID.
Runbook técnico completo + setup de una sola vez (WIF, rollout en orden, seguridad):
tcgcards-api/docs/deployment-flow.md en el repo.