Ir al contenido

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)

main es la fuente de verdad de prod. staging es solo una rama de despliegue = main + lo que tenga de test-only. Las tres reglas:

  1. Los features salen de main y se integran a main (vía PR). Integrar a main es seguro porque main no se despliega solo.
  2. Para probar en staging: la rama del featurestaging (git merge feat/... sobre staging + push) → staging se despliega.
  3. NUNCA staging → main.

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.

PiezaRepoDónde vivePor qué
test-login (entrar como usuario ficticio sin Google)websolo staging (commit que nunca se integra a main)Es un bypass de auth. Aunque está gateado fail-closed (VERCEL_ENV !== 'production' + el provider ni se registra), por política no toca el bundle de prod.
seam MP_FAKE_PAYMENTS (pagos sintéticos para la suite E2E)apimain, inerteMenos sensible (fake de pagos), auditado y con cobertura de unit-tests en CI. Fail-closed: en prod el flag está apagado y, si se prendiera, el server no arranca (exige no-prod + DB con “staging”).

El test-login es la razón concreta por la que nunca se fusiona stagingmain: ese commit vive en staging para siempre y jamás debe llegar a prod.


Cuándo: cada vez que quieres probar un cambio en un ambiente real sin tocar prod.

  1. Trabajas en una rama de feature (o directo en staging).
  2. Llevas los cambios a la rama staging del repo correspondiente:
    Ventana de terminal
    git checkout staging
    git merge tu-rama # o trabajas directo sobre staging
    git push origin staging
  3. 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 a staging → build → publica en staging.tcgcards.cl.

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 (usuario staging + la clave configurada en STAGING_BASIC_AUTH). Adentro, navegas como en prod pero contra la DB/credenciales de staging.

Cuándo: ya probaste en staging y quieres llevar el cambio a los usuarios.

Fusionas la rama del feature (NO staging) en main:

Ventana de terminal
git checkout main
git merge feat/tu-feature # ⬅ la RAMA del cambio, NUNCA `git merge staging`
git push origin main

2. 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 → branch mainRun.
  • Web → PROD: GitHub → tcgcards-web → pestaña Actions“Deploy Web → PROD (manual)”Run workflow → branch mainRun.

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.cl en incógnito → ver el cambio. En el log del run, vercel deploy imprime Aliased https://tcgcards.cl.

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é en staging pero nunca se integre a main, 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 stagingmain 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:

Ventana de terminal
# HOY — A a staging, sin prod
git 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 prod
git checkout staging && git merge feat/B && git push origin staging # staging = A + B; pruebas B
git 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.
  • staging es 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

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:

Ventana de terminal
gcloud run revisions list --service tcgcards-api \
--region us-central1 --project api-cards-prod --limit 5
gcloud run services update-traffic tcgcards-api \
--to-revisions=<REVISION_ANTERIOR>=100 \
--region us-central1 --project api-cards-prod

Crear/actualizar un Cloud Run Job + Scheduler

Sección titulada «Crear/actualizar un Cloud Run Job + Scheduler»

Los botones de deploy solo despliegan el servicio (tcgcards-api). Los Cloud Run Jobs —tareas que corren por agenda, como refresh-prices o auto-price (la marca visible es “Precios dinámicos”)— NO los crea el pipeline de deploy: se crean/actualizan a mano con gcloud. Este es el procedimiento tal como se montó auto-price el 2026-06-30.

Ventana de terminal
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>)
Ventana de terminal
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-prices

3. 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:

Ventana de terminal
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»
Ventana de terminal
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"
Ventana de terminal
gcloud scheduler jobs run <NOMBRE>-daily --location us-central1 --project api-cards-prod
gcloud 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).

  • 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 porque next build necesita las System env vars de Vercel (ej. VERCEL_URL) y falla con ERR_INVALID_URL.
  • API usa --image en CI (build+push de la imagen a Artifact Registry, luego gcloud 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.sh caen a --source si no hay IMAGE.)
  • 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.

PiezaValor
Web prodtcgcards.cl (Vercel, branch main, auto-deploy apagado → solo botón)
Web stagingstaging.tcgcards.cl (Vercel, branch staging, basic auth + noindex)
API prodtcgcards-api (Cloud Run) · https://tcgcards-api-1033181994095.us-central1.run.app
API stagingtcgcards-api-staging (Cloud Run) · https://tcgcards-api-staging-1033181994095.us-central1.run.app
Auth GitHub→GCPWorkload 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.