Pipeline de despliegue con GitHub Actions¶
Análisis, requerimientos y plan por fases para automatizar el despliegue del storefront: una imagen por entorno, construida en GitHub Actions, publicada en GHCR y desplegada por un runner self-hosted en el host. Reemplaza el procedimiento manual que hoy documenta el runbook —«No hay pipeline de deploy en este repositorio»—.
Alcance. Los hallazgos de la sección Diagnóstico están medidos contra
developy contra la imagen construida el 26/08/2026, no inferidos: cada fila dice dónde mirar. El plan por fases es una propuesta; las fases 2 y 3 dependen de definiciones del equipo que el repositorio no puede contestar y que están listadas al final.
Respuesta corta¶
Tres decisiones fijan la forma del pipeline. Se tomaron el 26/08/2026 y cada una cierra una bifurcación que cambiaba el diseño entero, no un detalle de implementación.
| Pregunta | Decisión | Por qué |
|---|---|---|
| Modelo de imagen | Una imagen por entorno | Las NEXT_PUBLIC_* se inlinean durante next build. Una imagen de staging ya tiene el índice _dev de Algolia horneado en el bundle: promoverla a producción es imposible cambiándole el .env |
| Mecanismo de despliegue | Runner self-hosted en el host | El job corre dentro de la red de Compulandia y solo hace docker pull + compose up. No hay clave SSH del servidor guardada en GitHub |
| Alcance del artefacto | Migrar a output: standalone |
Verificado en la fase 0: 1.28 GB → 264 MB, y REVALIDATE_SECRET deja de viajar dentro de la imagen |
| Disparadores | Push a develop para staging, tag v* para producción |
Staging necesita feedback rápido; producción, un acto deliberado. Sin PRs en el flujo del equipo, el tag es el punto de control |
| Fuente de las variables | GCP Secret Manager, un secret por entorno con el .env completo |
Un solo archivo para build y runtime, con historial de versiones legible. Los secrets de GitHub son write-only: no dejan ver el valor anterior |
La imagen única para ambos entornos no es posible sin refactor
Es la petición natural —una imagen que se despliegue en producción o en staging— y choca
con cómo funciona Next. El propio runbook lo advierte: las NEXT_PUBLIC_* se congelan en
el build. Hacerla promovible exige sacar del bundle las ~6 variables que difieren entre
entornos y servirlas en runtime. Queda registrado como evaluación de la fase 4, no como
parte de este trabajo.
Diagnóstico¶
Estado del artefacto¶
Instantánea del 26/08/2026, antes de la fase 0
Las filas sobre peso, npm install, node:20 y el secreto en la imagen quedaron resueltas
ese mismo día — ver la tabla R1 y el ADR-0005. Se dejan
escritas porque son el motivo de las decisiones que siguen.
| Hallazgo | Evidencia | Impacto en el pipeline |
|---|---|---|
| La imagen pesa 1.28 GB | node_modules 820 MB + .next 428 MB |
Cada build empuja ~1 GB al registry |
REVALIDATE_SECRET viajaba dentro de la imagen (resuelto en la fase 0) |
.next/cache/webpack/{server,client}-production/0.pack |
Cualquiera con permiso de pull lo extraía |
Se instala con npm install, no npm ci |
Dockerfile, etapa builder |
Dos builds del mismo commit pueden diferir |
La imagen se construye sobre node:20-alpine |
package.json declara engines: node >= 22 (ADR-0004, 26/08/2026) |
Se compila sobre una versión que el proyecto declara no soportada |
| No hay registry ni tags de imagen | Runbook, Rollback | Volver atrás exige recompilar: minutos |
| No hay endpoint de salud | Solo existe src/app/api/credit-form |
El runner no puede afirmar que el deploy funcionó |
| El host de deploy no está registrado en el repo | Runbook, Pendiente | No hay a dónde apuntar el runner todavía |
El secreto en el cache de webpack — resuelto en la fase 0
La etapa de runtime hacía COPY --from=builder /app/.next ./.next, que arrastraba también
.next/cache. Ese cache contiene el snapshot de entorno del build, con el valor de
REVALIDATE_SECRET en texto plano. Mientras la imagen no salía del host era un detalle;
se volvía un problema real el día que se publicara en un registry. output: standalone
lo resolvió de raíz: el cache no forma parte del output.
Lo que ya se corrigió¶
El build fallaba dentro de Docker por dos causas acumuladas, ambas arregladas y verificadas el 26/08/2026:
- No existía
.dockerignore.COPY . .metía 745 MB de.nextviejo y 821 MB denode_modulesdel host, pisando elnpm installde la etapa builder y haciendo que webpack reutilizara chunks compilados de builds anteriores. generateStaticParamsllamaba a Medusa sin protección. Cada caída del backend rompía el build entero, con el error saltando decategoriesacollectionssegún qué chunk se reutilizaba.
El build ahora termina incluso con el backend devolviendo 502 —comprobado, porque el backend de dev se cayó dos veces durante este mismo análisis—.
Arquitectura del pipeline¶
flowchart LR
subgraph sm["GCP Secret Manager"]
direction TB
ss["storefront-staging-env"]
sp["storefront-prod-env"]
end
subgraph gh["GitHub Actions"]
direction TB
pd["push a develop"] --> bs["build<br/>ubuntu-latest"]
bs -->|docker push| gs["GHCR<br/>:staging-SHA"]
pm["tag v1.3.0<br/>guard: sobre main"] --> bp["build<br/>ubuntu-latest"]
bp -->|docker push| gp["GHCR<br/>:v1.3.0 + :prod-SHA"]
end
subgraph infra["Infraestructura Compulandia"]
direction TB
rs["runner self-hosted<br/>VM red local"] --> hs["host staging"]
rp["runner self-hosted<br/>GCP"] --> hp["host produccion"]
end
ss -. NEXT_PUBLIC_* .-> bs
sp -. NEXT_PUBLIC_* .-> bp
gs -->|docker pull| rs
gp -->|docker pull| rp
ss -. escribe .env .-> rs
sp -. escribe .env .-> rp
Dos carriles independientes. Lo que los separa no es la configuración del contenedor sino el
momento del build: los build-args entran ahí, no en el run, y por eso hacen falta dos
imágenes.
El mismo secret alimenta los dos lados de cada carril —las NEXT_PUBLIC_* al build, el .env
completo al host— y por eso hay una sola fuente de verdad y no dos lugares que mantener
sincronizados. En la infraestructura de Compulandia nada se ejecuta en GitHub: el runner vive en
el host, tira de la imagen y escribe el .env.
Consecuencia que hay que asumir
Hoy el build funciona porque .env.local entra al contexto por COPY . .. En Actions ese
archivo no existe: está en .gitignore. El pipeline tiene que pasar las 11
NEXT_PUBLIC_* como ARG/ENV explícitos —tomadas del secret— y el .dockerignore pasa a
excluir .env* para que un archivo suelto en una máquina no se cuele en un build. Es el mismo
ARG que se quitó del Dockerfile el 26/08/2026, pero al revés: antes llegaba vacío y pisaba
el valor bueno de .env.local; ahora llega siempre con valor y es la única fuente.
Modelo de disparadores¶
Dos modelos conviviendo, cada uno donde corresponde. Staging sin ceremonia, porque es donde hay que ver los cambios rápido; producción con un acto explícito y versionado.
| Disparador | Imagen | Despliega | Guard |
|---|---|---|---|
push a staging |
staging-<sha> |
staging, automático | — |
tag v1.3.0 |
v1.3.0 y prod-<sha> |
producción | el tag debe estar sobre staging o sobre main |
workflow_dispatch |
— | redespliega una versión ya construida | — |
branches y tags juntos se comportan como OR, no como AND
Un tag de git no pertenece a ninguna rama: apunta a un commit, que puede ser alcanzable
desde varias ramas o desde ninguna. Por eso Actions no puede filtrar por rama en un evento de
tag, y declarar branches y tags en el mismo push hace que el workflow corra cuando
cualquiera de los dos filtros matchea:
on:
push:
branches: [main] # NO restringe el tag
tags: ['v*'] # corre con cualquier push a main O cualquier tag v*
La restricción por rama se verifica dentro del job:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # sin historia completa no hay ancestría que comprobar
- name: El tag debe apuntar a un commit de staging o de main
run: |
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
+refs/heads/staging:refs/remotes/origin/staging
if git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then rama=main
elif git merge-base --is-ancestor "$GITHUB_SHA" origin/staging; then rama=staging
else
echo "::error::El tag $GITHUB_REF_NAME no está sobre staging ni sobre main"
exit 1
fi
Aceptar tags desde staging permite desplegar una versión ya validada sin esperar el merge a
main. El costo: main deja de ser el registro de lo que corre en producción si el merge
se demora. La disciplina que lo compensa es mergear staging a main después de cada
despliegue.
Los dos guards cubren cosas distintas y se complementan: la deployment tag policy del Environment
—que aplica GitHub— garantiza que solo refs llamados v* desplieguen a producción, pero filtra por
nombre y no puede saber sobre qué rama está el tag. Eso lo comprueba el job.
Este modelo también mejora el rollback: el objetivo pasa a ser una versión legible («volvé a la
v1.2.4») en lugar de un hash (prod-a1b2c3d).
Requerimientos¶
listo es trabajo ya hecho y verificado; bloqueado depende de una definición del equipo.
R1 · El artefacto¶
| ID | Requerimiento | Por qué | Estado |
|---|---|---|---|
| R1.1 | Contexto de build aislado del host | Evita que .next y node_modules locales entren a la imagen |
listo |
| R1.2 | Base node:22-alpine |
El ADR-0004 fijó engines: node >= 22 y la imagen estaba en 20 |
listo |
| R1.3 | npm ci en vez de npm install |
Build reproducible. Verificado: el lockfile está sincronizado | listo |
| R1.4 | output: standalone |
264 MB en vez de 1.28 GB, sin .next/cache |
listo |
| R1.5 | Ningún secreto server-side dentro de la imagen | REVALIDATE_SECRET estaba en el cache de webpack |
listo |
| R1.6 | El build no depende de que Medusa responda | El backend cayó dos veces durante este análisis | listo |
R2 · El pipeline¶
| ID | Requerimiento | Por qué | Estado |
|---|---|---|---|
| R2.1 | Workflow que construye y publica en GHCR | Es el artefacto versionado que hoy no existe | listo |
| R2.2 | Tag por commit: staging-<sha> / prod-<sha> |
Habilita el rollback sin recompilar que el runbook pide | listo |
| R2.3 | Push a staging → staging; tag v* → producción |
Staging necesita feedback rápido; producción, un acto deliberado y versionado | listo |
| R2.7 | El tag v* debe apuntar a un commit de staging o de main |
Un tag no pertenece a ninguna rama: hay que verificarlo en el job | listo |
| R2.8 | Crear la rama staging y documentarla en AGENTS.md |
El modelo del equipo es develop para features, staging para pruebas y aceptación |
listo |
| R2.4 | Cache de capas entre builds | Sin cache, cada build reinstala todo | listo |
| R2.5 | concurrency por entorno |
Dos pushes seguidos no pueden pisarse el deploy | listo |
| R2.6 | workflow_dispatch con tag a desplegar |
Rollback y redeploy sin tocar el host | listo |
R3 · Entorno y secretos¶
| ID | Requerimiento | Por qué | Estado |
|---|---|---|---|
| R3.1 | Un secret por entorno en GCP Secret Manager, con el .env completo como payload |
Una sola fuente para el build y para el runtime | listo |
| R3.2 | Workload Identity Federation entre GitHub y GCP | Evita guardar una clave de servicio en GitHub o en los hosts | listo |
| R3.3 | El job de build extrae las NEXT_PUBLIC_* del secret y las pasa como build-args |
En CI no existe .env.local: está en .gitignore |
listo |
| R3.4 | El job de deploy escribe el .env en el host antes de levantar |
Elimina la deriva entre lo configurado y lo que corre | listo |
| R3.5 | Environments staging y production en GitHub |
Habilitan la deployment tag policy y el registro de despliegues | listo |
| R3.6 | .dockerignore excluye .env* |
Un archivo suelto en una máquina no puede colarse a un build | listo |
| R3.7 | Definir las variables que el runbook documenta y no existen | PAYLOAD_USER_EMAIL, PAYLOAD_USER_PASSWORD, NEXT_PUBLIC_RECAPTCHA_SITE_KEY |
pendiente — la home funciona sin ellas |
| R3.8 | Un solo nombre por rol para la URL del backend | Eran tres; NEXT_PUBLIC_MEDUSA_BACKEND_URL se eliminó el 27/08/2026 |
listo |
De dónde salen las variables al compilar¶
En Actions no hay ningún archivo de entorno: .env.local está en .gitignore y nunca llega al
runner. Las variables se dividen en dos grupos que viajan por caminos distintos, desde el mismo
secret:
| Grupo | Cuándo se necesita | Cómo llega | ¿Queda en la imagen? |
|---|---|---|---|
Las 11 NEXT_PUBLIC_* |
Build — Next las inlinea en el bundle | build-args que el job extrae del secret |
Sí, horneadas |
Server-side (ALGOLIA_ADMIN_API_KEY, RECAPTCHA_SECRET_KEY, REVALIDATE_SECRET, PAYLOAD_USER_*) |
Runtime | El job de deploy escribe el .env en el host |
No |
Los server-side no tienen nada que hacer en el build, y mantenerlos fuera de la imagen es justamente lo que aseguró la fase 0.
El .env del host pasa a ser generado
A partir de la fase 3, editar el .env por SSH deja de tener efecto: el próximo despliegue lo
reescribe desde el secret. Es deliberado —así desaparece la deriva entre lo que alguien cree
que está configurado y lo que realmente corre— pero cambia el hábito: para cambiar un valor se
edita el secret y se redespliega.
Los runners self-hosted también reciben el token OIDC de GitHub
Es lo que hace viable este modelo con un host fuera de GCP. Ni la VM de la red local ni la de GCP necesitan una clave de servicio: la credencial es el token del job, que vive minutos. La VM local solo necesita salida HTTPS hacia las APIs de GCP.
R4 · Despliegue y verificación¶
| ID | Requerimiento | Por qué | Estado |
|---|---|---|---|
| R4.1 | Runner self-hosted por host, con labels | El job de deploy tiene que correr dentro de la red | bloqueado |
| R4.2 | Endpoint /api/health que no llame a Medusa |
/py da 500 con el backend caído: no sirve como señal |
listo |
| R4.3 | Smoke test después del up -d |
Un deploy que no se verifica no es un deploy | listo |
| R4.4 | El tag es el gate, reforzado con deployment tag policy v* |
Crear el tag es el acto explícito; la policy impide que otro ref despliegue a producción | listo |
| R4.5 | Rollback documentado por tag | Reemplaza el «volver al commit y recompilar» del runbook | listo |
| R4.6 | El compose despliega desde una imagen del registry |
Sin image:, docker compose pull no tiene de dónde bajar |
listo |
Lo que cambia en el rollback¶
Es el beneficio más concreto, y el que el propio runbook lista como mejora pendiente: «taguear imágenes por commit para poder volver sin recompilar».
| Hoy · en el host, sobre el código | Con registry · tag por commit | |
|---|---|---|
| Pasos | git checkout <sha> → npm install → next build → compose up -d |
elegir tag anterior → docker pull → compose up -d |
| Orden de magnitud | minutos | segundos |
| Por qué | Instalar y compilar ocurren en el momento del rollback | Ya ocurrieron una vez, cuando se construyó esa imagen |
Plan de implementación¶
Las fases son una secuencia real: cada una deja algo verificable y la siguiente se apoya en eso. Las fases 0 y 1 no tocan GitHub Actions — arreglan el artefacto antes de automatizar su publicación, que es el orden correcto: automatizar la publicación de una imagen que filtra un secreto solo hace que el problema se replique más rápido.
Fase 0 · Higiene del artefacto — completada el 2026-08-26¶
Requerimientos R1.2 – R1.5. Registrada en el ADR-0005.
- Subir la base a
node:22-alpineen ambas etapas, alineando con elenginesdel ADR-0004. - Cambiar
npm installpornpm ci. - Agregar
output: "standalone"anext.config.js. - Reescribir la etapa de runtime: copiar
.next/standalone,.next/staticypublic/; arrancar connode server.js. - Confirmar si
check-env-variables.jssigue corriendo al arrancar en modo standalone. Si no, mover el chequeo a un entrypoint — no perder la validación. - Registrar la decisión en un ADR nuevo. Las reglas del índice de ADRs
prohíben editar un ADR para cambiar la decisión: se escribe uno que lo reemplaza y el
viejo pasa a
reemplazado por ADR-NNNN.
Verificado el 2026-08-26
Imagen de 264 MB (desde 1.28 GB); grep de los tres secretos server-side devuelve 0
archivos y .next/cache ya no existe en la imagen; el contenedor arranca en el puerto
8000, sirve .next/static y public/, aborta con exit 1 si falta la publishable key
y responde a SIGTERM en 284 ms. Con el backend disponible y un build limpio: 687 páginas
prerenderizadas, todas las rutas probadas en 200, contenido del CMS en la home y el
optimizador de imágenes funcionando. Detalle en el
ADR-0005.
Fase 1 · Señal de salud propia — completada el 2026-08-27¶
Requerimiento R4.2.
- Crear
/api/healthque responda 200 sin llamar a Medusa ni a Payload, devolviendo el SHA del commit y la versión. - Inyectar el SHA como
ARGen el build para que la respuesta diga qué imagen está corriendo. - Agregar
healthcheckal servicio endocker-compose.yml.
Verificado el 2026-08-27
Con MEDUSA_BACKEND_URL apuntando a un puerto muerto, /api/health responde 200 y /py
responde 500. La ruta no hace ningún fetch saliente (verificado con
logging.fetches.fullUrl), devuelve Cache-Control: no-store y el healthcheck del compose
queda en healthy.
Usar 127.0.0.1 en el healthcheck, nunca localhost
Dentro del contenedor localhost resuelve a ::1 (IPv6) y el servidor escucha en 0.0.0.0
(IPv4). busybox wget no reintenta con la otra familia, así que con localhost el
healthcheck falla siempre, con la aplicación funcionando perfecto — y en el pipeline eso
se ve como un deploy que falla sin causa aparente.
Fase 2 · Construir y publicar — completada el 2026-08-27¶
Requerimientos R2.1 – R2.4 y R3.1 – R3.4.
- Crear los Environments
stagingyproductioncon sus variables y secretos. - Declarar las
NEXT_PUBLIC_*comoARG/ENVen elDockerfiley pasarlas desde el Environment. - Excluir
.env*del.dockerignore, ahora que las variables llegan porbuild-args. - Workflow
imagen.yml: dispara enpushadevelopymain, login a GHCR con elGITHUB_TOKEN, build y push con tag por SHA y tag móvil por entorno. - Cache de capas entre corridas para que el build no reinstale de cero.
Criterio de verificación
Un push a develop deja la imagen en GHCR; un docker pull desde el host la baja y levanta
el sitio con los valores de staging correctos — concretamente, con el índice de Algolia de
staging, no el de producción.
Fase 3 · Desplegar desde el runner — completada el 2026-08-28¶
Requerimientos R4.1 y R4.3 – R4.6.
- Instalar el runner self-hosted en cada host con labels (
storefront+staging/production). - Job
deployque depende del build, corre sobre esos labels y hacecompose pull+up -dcon el tag por SHA. - Configurar el
environment: productioncon deployment tag policyv*, para que ningún otro ref pueda desplegar ahí. Revisores requeridos quedan sin activar: con un equipo chico, quien taguea y quien aprueba son la misma persona. - Smoke test contra
/api/health, y fallar el job si no responde. - Reescribir la sección Deploy del runbook, que hoy dice que no hay pipeline, y completar su sección Pendiente con los hosts reales.
Criterio de verificación
Un merge a main queda esperando aprobación; al aprobarlo, el sitio de producción sirve el
commit nuevo y /api/health devuelve ese SHA. Un workflow_dispatch con el tag anterior
revierte en segundos.
Fase 4 · Endurecer¶
Requerimiento R2.5. Después de la primera semana en uso.
concurrencypor entorno para que dos deploys no se pisen.- Política de retención de imágenes en GHCR: el registry crece con cada commit.
- ADR del pipeline, dentro del mismo PR que lo implementa, como pide
AGENTS.md. - Evaluar si conviene mover a imagen única promovible. La decisión se retoma con datos de uso real, no antes.
Trampas encontradas al implementarlo¶
Seis cosas que costaron tiempo y que no se deducen leyendo documentación. Cinco de las seis no fallan de forma obvia: producen un éxito aparente, un log confuso o una espera indefinida.
| Trampa | Qué se ve | Qué pasa en realidad |
|---|---|---|
build-args parsea una clave por línea |
El build falla diciendo que falta la publishable key, aunque el secret la trae | Un valor multilínea se parte: llega solo la primera variable y el resto se lee como build-args que el Dockerfile no declara, y se descartan en silencio |
github.repository conserva las mayúsculas |
repository name must be lowercase al pushear |
GHCR exige minúsculas; hay que normalizar con tr |
Dentro del contenedor, localhost resuelve a ::1 |
El healthcheck falla siempre, con la aplicación sana | El servidor escucha en 0.0.0.0 (IPv4) y busybox wget no reintenta con la otra familia. Usar 127.0.0.1 |
| Label de runner equivocada | El job queda en Queued, sin error | No falla: espera indefinidamente un runner que no existe |
| Las variables server-side no se pueden validar en el build | Contenedor healthy, todas las páginas en 500, despliegue reportado como exitoso |
No son build-args, así que exigirlas rompería la compilación. Se validan al arrancar, con --runtime |
| El WAF desafía al middleware | Unexpected token '<' … is not valid JSON |
El fetch server-side sale con User-Agent Next.js Middleware desde una IP de datacenter; Cloudflare responde un managed challenge en HTML. Ver la falla 5 del runbook |
La lección transversal: un pipeline no falla donde uno mira. La mitad de estos casos terminaban en un despliegue "exitoso" con el sitio roto, y por eso las dos validaciones que se agregaron —la de runtime y la de render— valen más que cualquier otra parte del trabajo.
Deuda que el pipeline hereda¶
Ninguna bloquea el trabajo, pero todas se vuelven más caras una vez que el deploy es automático: un valor mal puesto deja de ser el error de una persona en un host y pasa a replicarse solo.
| Deuda | Qué pasa en el pipeline | Severidad |
|---|---|---|
| ~~Tres nombres para la URL del backend~~ Resuelto el 27/08/2026 | Quedan dos, uno por rol: MEDUSA_BACKEND_URL (server, runtime) y NEXT_PUBLIC_MEDUSA_BASE_URL (navegador, build) |
— |
Variables que el runbook documenta y no existen en .env.local |
El primer deploy levanta con la home vacía si Payload autentica | alta |
NEXT_PUBLIC_ALGOLIA_API_KEY, que el código lee y nadie define |
La búsqueda queda rota en ambos entornos por igual | media |
| El backend de dev responde 502 de forma intermitente | El build ya no falla, pero un smoke test contra /py sí lo haría — de ahí /api/health |
media |
handleGoogleCallback importada y nunca exportada |
Warning en build, error en runtime al usar login con Google | media |
El build ignora errores de ESLint y TypeScript (ignoreDuringBuilds, ignoreBuildErrors) |
El pipeline publica igual con código que no compila en estricto | baja |
Definiciones resueltas¶
Respondidas por el equipo entre el 26 y el 27/08/2026:
| Pregunta | Respuesta |
|---|---|
| ¿Host de staging separado? | Sí. Staging es una VM en la red local de desarrollo; producción está en GCP. Dos runners self-hosted con labels distintos |
| ¿Qué rama dispara staging? | staging, que hay que crear. El modelo del equipo es develop para empujar y mergear features, staging para pruebas y aceptación, main por defecto y poco usada |
| ¿Desde qué rama se puede taguear para producción? | Desde staging o desde main. Es la consecuencia de que main se use poco: exigir solo main bloquearía el despliegue de versiones ya aceptadas |
¿Quién puede crear tags v*? |
Cualquier dev con acceso al repo. No hay restricción de personas: el control es la deployment tag policy más la verificación de rama |
| ¿Qué valores difieren entre entornos? | Potencialmente todos los del .env. Por eso hay un secret completo por entorno y no una lista de excepciones |
| ¿Dónde viven las variables? | GCP Secret Manager, con el .env completo como payload |
| ¿Existe el proyecto de GCP? | Sí, con Secret Manager ya en uso. Falta habilitar la lectura desde GitHub (Workload Identity Federation) |
| Nombres de los secrets | storefront-staging-env y storefront-prod-env |
Definiciones pendientes¶
Ninguna. Las que quedaban se resolvieron entre el 27 y el 28/08/2026: la URL del backend de
producción entró con el secret storefront-prod-env, y la lectura desde GitHub quedó habilitada
con Workload Identity Federation.