Compare commits

..
Author SHA1 Message Date
devops d1a4a84d24 feat(ecommerce): revalidar el storefront al instante tras editar precios
Antes el precio actualizado en Medusa tardaba hasta ~60s (o más, con
varias réplicas del frontend sin cache handler compartido) en verse en
el front, por el revalidate:60 del fetch a /store/products.

Ahora:
- Nueva ruta app/api/revalidate en el storefront que llama a
  revalidateTag("medusa-products") al recibir un POST autenticado con
  el header x-revalidate-secret.
- Nuevo subscriber en el backend Medusa (product.updated,
  product-variant.updated/created/deleted) que llama a esa ruta usando
  STOREFRONT_URL (ya existe en commerce-config) y un REVALIDATE_SECRET
  compartido entre ambos servicios.
- secrets.template.yaml documenta la nueva clave REVALIDATE_SECRET en
  commerce-secrets y commerce-storefront (mismo valor en ambos).

Requiere reaplicar commerce-secrets y commerce-storefront con el script
actualizado en el repo scripts (fix/revalidate-secret-scripts) para que
el REVALIDATE_SECRET exista en el cluster.
2026-08-13 16:02:49 -05:00
devops a9c57d4985 chore: release v1.0.89 [skip ci] 2026-08-13 07:07:55 +00:00
devops 584a42ea72 Merge pull request 'fix(ecommerce): detalle de producto en dynamic render, no SSG+ISR stale' (#8) from fix/product-detail-static-mock-price into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 5m55s
2026-08-13 07:02:11 +00:00
devopsandClaude Sonnet 5 a6a92f75ea fix(ecommerce): render product detail dynamically instead of stale SSG+ISR
Product detail pages were generateStaticParams'd at Docker build time,
where MEDUSA_* env vars aren't set, so the static snapshot baked in the
mock catalog (price: null for all products). With revalidate=60 and no
shared ISR cache handler across the 2 frontend replicas, each pod healed
its own cache independently on next visit past staleness — so a product
could show "Consultar precio" on one pod and the real price on the other,
while the fully-dynamic catalog listing always hit Medusa fresh. Verified
against the live Medusa API that pricing/region data itself was correct
for the reported failing products.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DXqBoNfYQNwFg65mbx2FWM
2026-08-13 01:59:44 -05:00
devops 0eea6234d6 chore: release v1.0.87 [skip ci] 2026-08-13 00:24:20 -05:00
gitea-actions 77bf6089fb chore(gitops): deploy Medusa v1.0.88 [skip ci] 2026-08-13 05:01:13 +00:00
devops 0134a4dd0b Merge pull request 'fix: desactivar cookie Secure en sesion admin de Medusa' (#7) from fix/medusa-admin-session-cookie into main
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 18m10s
2026-08-13 04:08:03 +00:00
devops 3fe63591ec Merge pull request 'fix: forzar ISR en /product/[slug] para que no quede atascado en mock' (#6) from fix/product-page-static-mock-price into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m12s
2026-08-13 04:04:40 +00:00
devops c6cf394c26 fix: desactivar cookie Secure en sesion admin de Medusa
Todos los Ingress del lab terminan en Traefik por HTTP plano (entrypoint
web, sin TLS); Cloudflare es quien atiende HTTPS de cara al navegador.
Con secure:true (default en produccion), express-session nunca emite
Set-Cookie porque no detecta la conexion como HTTPS, dejando el
dashboard admin con 401 en /admin/users/me pese a loguear bien.
2026-08-12 23:01:35 -05:00
devops 7a09202c27 fix: forzar ISR en /product/[slug] para que no quede atascado en datos mock del build
generateStaticParams usa el catálogo mock en build time porque
HEADLESS_PROVIDER y MEDUSA_* no existen como build args del Dockerfile.
Sin un revalidate explícito, esa ruta queda estática para siempre con
price:null. El listado no sufre esto porque su ruta es dinámica por
searchParams.
2026-08-12 22:56:03 -05:00
devops a902dc0a26 chore: release v1.0.86 [skip ci] 2026-08-12 07:16:46 +00:00
devops 04dc6bd8bc Merge pull request 'fix: usar region_id en vez de currency_code para precios de Medusa' (#5) from fix/medusa-region-id-pricing into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 9m9s
2026-08-12 07:03:28 +00:00
devops 60072cfcbe fix: usar region_id en vez de currency_code para precios de Medusa
El validador de /store/products (StoreGetProductsParams, .strict()) solo
acepta region_id/country_code/province/cart_id para resolver contexto de
precio -- currency_code como campo plano no existe y rompía con 400
"Unrecognized fields: 'currency_code'" en cada carga de /catalog.

country_code tampoco alcanza: probado en vivo, devuelve "Missing required
pricing context to calculate prices - region_id". Se necesita region_id
explícito.

Se agrega MEDUSA_REGION_ID (env var) apuntando a la región "Colombia"
(COP) creada vía Admin API, ya que no existía ninguna región configurada
en el backend.
2026-08-12 02:01:49 -05:00
devops c9b376c4e6 Merge pull request 'fix: conectar el frontend al catálogo real de Medusa' (#4) from fix/frontend-wire-medusa-catalog into main 2026-08-12 05:16:18 +00:00
devops cea41ce428 fix: conectar el frontend al catálogo real de Medusa
El Deployment frontend-deploy no tenía ninguna variable de entorno, así
que HEADLESS_PROVIDER quedaba undefined y el provider caía siempre en
mockCatalogProvider (data/products.json estático), aunque
medusaCatalogProvider ya estaba implementado y listo en
lib/headless/providers/medusa.ts.

Se agrega HEADLESS_PROVIDER=medusa, MEDUSA_BACKEND_URL apuntando al
Service interno medusa-svc:9000 (evita depender de Cloudflare/NPM en
cada render SSR), y envFrom hacia el secret commerce-storefront
(MEDUSA_PUBLISHABLE_KEY) ya creado en el namespace ecommerce.

No se bumpea el tag de imagen: build.yaml excluye explícitamente los
cambios de frontend.yaml del trigger de build, y la versión la gestiona
la propia CI vía sed sobre este archivo.
2026-08-12 00:12:10 -05:00
8 changed files with 115 additions and 9 deletions
@@ -25,6 +25,16 @@ module.exports = defineConfig({
jwtSecret: required("JWT_SECRET"),
cookieSecret: required("COOKIE_SECRET"),
},
// Todos los Ingress del lab terminan en Traefik por HTTP plano
// (entrypoint "web", sin TLS); Cloudflare es quien atiende HTTPS de
// cara al navegador. Con `secure: true` (el default en producción),
// express-session nunca emite Set-Cookie porque no detecta la
// conexión como HTTPS, y el dashboard admin queda con 401 en
// /admin/users/me pese a loguear bien. La API con Bearer token no se
// ve afectada por esto.
cookieOptions: {
secure: false,
},
},
admin: {
@@ -0,0 +1,44 @@
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework";
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
export default async function revalidateStorefrontHandler({
container,
}: SubscriberArgs) {
const logger = container.resolve(ContainerRegistrationKeys.LOGGER);
const storefrontUrl = process.env.STOREFRONT_URL;
const secret = process.env.REVALIDATE_SECRET;
if (!storefrontUrl || !secret) {
logger.warn(
"STOREFRONT_URL o REVALIDATE_SECRET no configurados; se omite la revalidación del storefront.",
);
return;
}
try {
const response = await fetch(`${storefrontUrl}/api/revalidate`, {
method: "POST",
headers: { "x-revalidate-secret": secret },
});
if (!response.ok) {
logger.warn(
`Revalidación del storefront respondió HTTP ${response.status}.`,
);
}
} catch (error) {
logger.warn(
`No se pudo revalidar el storefront: ${(error as Error).message}`,
);
}
}
export const config: SubscriberConfig = {
event: [
"product.updated",
"product-variant.updated",
"product-variant.created",
"product-variant.deleted",
],
};
@@ -0,0 +1,13 @@
import { revalidateTag } from "next/cache";
export async function POST(request: Request) {
const secret = request.headers.get("x-revalidate-secret");
if (!secret || secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: "Invalid secret" }, { status: 401 });
}
revalidateTag("medusa-products");
return Response.json({ revalidated: true, tag: "medusa-products" }, { status: 200 });
}
@@ -9,10 +9,17 @@ import { getProductBySlug, getProducts } from "@/lib/headless/client";
interface ProductPageProps { params: Promise<{ slug: string }> }
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({ slug: product.slug }));
}
// generateStaticParams + revalidate quedaban aquí para pre-renderizar en
// build time, pero HEADLESS_PROVIDER/MEDUSA_* no existen en el contexto del
// Docker build: el snapshot estático quedaba congelado con el catálogo mock
// (todos los productos con price: null) hasta que ISR lo revalidara. Con
// frontend-deploy corriendo 2 réplicas y sin cacheHandler compartido, cada
// pod revalida su propia caché de forma independiente, así que un producto
// podía mostrar "Consultar precio" en un pod y el precio real en el otro
// según cuál hubiera sido "calentado". force-dynamic elimina esa caché por
// pod y deja esta ruta en vivo contra Medusa, igual que el listado
// (dinámico por searchParams).
export const dynamic = "force-dynamic";
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params;
+2 -2
View File
@@ -52,7 +52,7 @@ spec:
memory: 64Mi
- name: migrations
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.85
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.88
imagePullPolicy: IfNotPresent
command:
- npx
@@ -73,7 +73,7 @@ spec:
containers:
- name: medusa
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.85
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.88
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
@@ -22,6 +22,11 @@ stringData:
JWT_SECRET: CAMBIAR
COOKIE_SECRET: CAMBIAR
# Debe ser el MISMO valor que REVALIDATE_SECRET en el Secret
# commerce-storefront: el subscriber de Medusa lo envía como header
# x-revalidate-secret al invocar POST /api/revalidate en el storefront.
REVALIDATE_SECRET: CAMBIAR
---
apiVersion: v1
kind: Secret
@@ -30,3 +35,6 @@ metadata:
type: Opaque
stringData:
MEDUSA_PUBLISHABLE_KEY: pk_CAMBIAR_DESPUES_DE_CREARLA_EN_MEDUSA
# Mismo valor que REVALIDATE_SECRET en commerce-secrets.
REVALIDATE_SECRET: CAMBIAR
+11 -1
View File
@@ -16,9 +16,19 @@ spec:
- name: gitea-registry-secret
containers:
- name: web
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.77
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.89
ports:
- containerPort: 80
env:
- name: HEADLESS_PROVIDER
value: medusa
- name: MEDUSA_BACKEND_URL
value: http://medusa-svc:9000
- name: MEDUSA_REGION_ID
value: reg_01KZT86WPAH7A7ZZRDSX3350V2
envFrom:
- secretRef:
name: commerce-storefront
---
apiVersion: v1
kind: Service
@@ -258,6 +258,14 @@ function backendUrl(): string {
return value.replace(/\/$/, "");
}
function regionId(): string {
const value = process.env.MEDUSA_REGION_ID;
if (!value) {
throw new Error("MEDUSA_REGION_ID no está configurado.");
}
return value;
}
async function medusaFetch<T>(
path: string,
searchParams?: URLSearchParams,
@@ -515,7 +523,10 @@ async function fetchProducts(
): Promise<Product[]> {
const params = new URLSearchParams({
limit: String(limit),
currency_code: "cop",
// El validador de /store/products no acepta currency_code (400
// "Unrecognized fields"); el precio calculado solo se resuelve con
// region_id.
region_id: regionId(),
fields:
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
});
@@ -543,7 +554,10 @@ export const medusaCatalogProvider: CatalogProvider = {
const params = new URLSearchParams({
handle: slug,
limit: "1",
currency_code: "cop",
// El validador de /store/products no acepta currency_code (400
// "Unrecognized fields"); el precio calculado solo se resuelve con
// region_id.
region_id: regionId(),
fields:
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
});