Sin uso actual, y el host está evaluando capacidad de memoria para la
Etapa 2 del ADR 0006 (Meilisearch) con swap bajo presión. Se quita del
app-of-apps para liberar los recursos del namespace `monitoring`
(Grafana, Prometheus, Alertmanager, kube-state-metrics,
prometheus-operator -- estos dos últimos venían en CrashLoopBackOff
desde hace 37 días, ~8000 reinicios cada uno, conexión rechazada
contra el apiserver in-cluster -- y los 3 node-exporter).
Reversible: reagregar ambos archivos re-despliega el stack completo
vía Helm/kustomize sin pasos manuales. Sin pérdida de datos -- el
namespace monitoring no tenía ningún PVC, confirmado antes de borrar
(kubectl -n monitoring get pvc -> vacío).
Se agregó el finalizer resources-finalizer.argocd.argoproj.io a ambas
Applications (monitoring-app, monitoring-governance) antes de este
commit para que Argo CD borre en cascada los recursos desplegados al
podarlas, no solo el objeto Application -- sin eso quedarían pods
huérfanos corriendo sin liberar memoria.
commerce.cruzcloud.net (medusa-svc) has no route at "/" — Medusa only
serves /health, /app (admin), /store, etc. lab-agent's generic public
domain check flagged the resulting 404 as an anomaly every cycle even
though the service is healthy.
Adds a Traefik Middleware (redirectRegex) scoped to the commerce host
that 301s "/" to "/app", so the DNS/routing layer owns the entry point
instead of every client (including monitoring) having to know to
append /app manually. Regex includes the host explicitly because the
same Ingress also routes media.cruzcloud.net and shares
router.middlewares at the Ingress-annotation level.
Documenta el cambio a Telegram (silencioso para info, hilos al
recuperarse, log adjunto), la pausa nocturna 06:00-23:00 con su
tensión sin resolver con la ventana de medusa-deploy, la activación
del timer de systemd (60 min, bajado de 15 para reducir costo) con el
gotcha de Environment=HOME= para systemd --user, y el costo mensual
estimado actualizado.
17 corridas/día en vez de 68. Nota importante: el costo por corrida
sube al perder la ventaja del cache tibio de Claude Code (~1h TTL,
corridas espaciadas 60 min lo pierden casi siempre) -- el ahorro real
es ~1.5-2x, no 4x. También documenta el trade-off de latencia de
detección (~70 min en vez de ~25 min hasta detectar un problema).
Timer activo desde 2026-08-26. Documenta el hallazgo en vivo: el
manager de systemd --user de devops arranca con HOME=/DATA (el de
/etc/passwd), no con el override de /etc/profile.d/zz-devops-home.sh
que solo aplica a shells interactivas -- sin Environment=HOME=... en
la unidad, claude -p habría fallado la autenticación en cada corrida
del timer, silenciosamente. Se detectó y corrigió antes de activar,
corriendo el servicio manualmente vía systemctl --user start con el
entorno real como último paso de validación.
Actualiza la sección de Notificaciones: formato con íconos por tipo de
evento, log completo adjunto como documento, nivel "info" silencioso
(disable_notification), y respuestas en hilo cuando un recurso
accionado se recupera solo -- con la limitación conocida de que un
desbloqueo manual (borrando la entrada de state.json) pierde el
message_id y no queda threadeado. Validado en vivo con el canario.
Actualiza docs/arquitectura/agente-monitoreo.md: canal de notificación
cambiado de ntfy a Telegram Bot API (con la evidencia de la prueba de
entrega), horario activo 06:00-23:00 con su justificación de costo, la
tensión sin resolver con la ventana de medusa-deploy (00:00-06:00, no
se solapa con el horario activo del agente), y la estimación de costo
mensual (~US$60-65, basada en las corridas reales medidas).
Documentación de gobernanza y arquitectura del agente lab-agent:
lista blanca aprobada, diseño de seguridad (--allowedTools "" en vez
de un Bash tool con patrón, por la vulnerabilidad de coincidencia por
prefijo encontrada en vivo), autenticación por login de suscripción, y
evidencia completa de la prueba end-to-end del 2026-08-26.
Agrega docs/arquitectura/agente-monitoreo.md como referencia técnica y
documento de gobernanza del wrapper headless de diagnóstico/corrección
(scripts/lab-agent). Cubre la lista blanca de acciones automáticas
aprobada, por qué Claude corre con --allowedTools "" en vez de un Bash
tool con prefijo (coincidencia por prefijo permite inyección de
comandos, confirmado en vivo), el rediseño de autenticación a login de
suscripción en vez de API key, y la evidencia completa de la prueba
end-to-end del 2026-08-26 (DRY_RUN, ejecución real, rate limit/bloqueo,
2 bugs encontrados y corregidos).
Estado seguia diciendo "propuesta -- pendiente de aprobacion antes de
Etapa 2" aunque las 3 sub-fases (datos/categorias, navegacion,
componentes visuales -- PRs #29-#42) ya estan implementadas y en
produccion desde el 2026-08-25. Actualiza el header de estado y
reemplaza la seccion 6 (antes "Proximos pasos" pendiente de
aprobacion) por el resumen real de lo ejecutado, dejando explicito que
sigue pendiente borrar el usuario admin temporal de la auditoria y que
no hay Etapa 3 definida todavia.
tsconfig tiene noUncheckedIndexedAccess -- promoRoots[promoIndex %
promoRoots.length] tipa como Category | undefined aunque en runtime
siempre es valido (promoRoots.length > 0 ya se verifico antes de este
punto). Extraido a una constante con guard explicito para que TS
angoste el tipo. Sin cambio de comportamiento.
Detectado por el build real en main tras el merge de #41 -- no se
puede correr tsc localmente en esta maquina (ver notas de sesiones
anteriores), asi que esto solo lo valida el CI post-merge.
ADR 0005 seccion 3 (patron Walmart), ultimo item de la sub-fase de
componentes visuales: "grid de catalogo con tiles promocionales
intercalados, no solo tarjetas de producto homogeneas".
`category-tiles.tsx` ya era ese mismo primitivo visual (usado en el
home) -- se extrajo el tile individual a un componente exportado
`PromoTile` (mismo look, sin cambio visual en el home) para
reutilizarlo en `catalog-client.tsx`: un tile cada
PROMO_TILE_INTERVAL=8 productos, ciclando por las categorias raiz
(excluye la raiz que el usuario ya esta viendo via el breadcrumb de
#40). Si no hay jerarquia de categorias disponible, la grilla queda
igual que antes (solo productos) -- degradacion sin romper nada.
Depende de #40 (breadcrumb + categoryNodes en CatalogClient) -- mismo
archivo, se apila sobre esa rama en vez de main.
ADR 0005 seccion 3 (patron Amazon), primer item de "Filtros facetados
en sidebar con breadcrumb de categoria":
- Breadcrumb (Inicio > raiz > hija) armado caminando parentId sobre la
jerarquia real de categorias (misma que ya consume el mega-menu de
navbar.tsx), no un dato nuevo -- CatalogClient ahora recibe el
arreglo `categories: Category[]` completo ademas del
`categoryGroups` ya existente
- Conteo por valor en cada filtro de la sidebar (Categoria/Marca/Edad),
ej. "Marca (12)": se calcula client-side sobre los productos que ya
pasan el resto de filtros activos, cada dimension excluyendose a si
misma (patron faceted-count estandar). Sin Meilisearch por ahora,
como preveia el ADR ("server-side simple por ahora")
Filtrado existente refactorizado a funciones matchesX() puras y
reutilizadas tanto por `filtered` como por los conteos, sin cambiar el
comportamiento de filtrado ya validado.
Expone ProductVariant.inventoryQuantity (ya calculado desde
variant.inventory_quantity en Medusa, hoy colapsado al booleano
available) y lo usa para mostrar "Quedan N" cuando el stock real es
bajo (<=5), tanto en la tarjeta de producto como en la ficha. null
indica inventario no gestionado (backorder / manage_inventory=false),
donde nunca se muestra un numero. ADR 0005 seccion 3 (patron eBay).
Catalogo mock actualizado (data/products.json) con
inventoryQuantity=null en todas las variantes para seguir tipando
contra Product[] via `satisfies`.
Reutiliza Product.variants[].{name,value,available}, ya expuesto por el
provider de Medusa (variant.options[0]), para swatches clicables en la
tarjeta del catalogo/home -- no solo en la ficha de producto. Solo se
muestra cuando el producto tiene mas de una variante; la variante
seleccionada se usa al agregar al carrito. ADR 0005 seccion 3 (patron
eBay), cero cambio de modelo de datos.
Reutiliza Product.compareAtPrice (ya calculado en el provider de Medusa
via original_amount > calculated_amount) para mostrar precio tachado +
badge de descuento en la grilla/catalogo, no solo en la ficha de
producto. ADR 0005 seccion 3, patron Walmart -- cero cambio de modelo
de datos.
Reemplaza la fila horizontal de pildoras por categoria (ADR 0005 seccion
3, patron Walmart) por un trigger unico "Departamentos" que abre un
panel multi-columna con las 6 categorias raiz y sus hijos, activado por
hover (mouse) o tap (touch), con cierre por click-fuera y Escape.
Bug preexistente (no introducido en esta sesion, solo expuesto): "/" se
prerenderizaba en build time dentro del Dockerfile, donde
HEADLESS_PROVIDER/MEDUSA_* no existen todavia (solo se inyectan en runtime
via el Deployment de k8s) -- el provider caia a "mock" y ese HTML quedaba
cacheado 1 año (confirmado en vivo: x-nextjs-prerender:1,
Cache-Control: s-maxage=31536000, x-nextjs-cache:HIT). El carrusel de
"Novedades" del home ya mostraba productos mock desde antes; recien se
noto con el nav data-driven de la sub-fase 2 porque las categorias mock
("Figuras"/"Peluches"/"Mochilas") son obviamente distintas a las reales.
/catalog y /product/[slug] no sufren esto porque ya son dinamicos
(searchParams / params fuerzan SSR).
export const dynamic = "force-dynamic" en app/page.tsx y app/layout.tsx
(el layout comparte el fetch del Navbar con TODAS las rutas -- forzarlo
ahi tambien protege contra que una pagina futura reintroduzca el mismo
bug). Decision del usuario: priorizar datos reales sobre el cacheo
estatico de 1 año.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013dunTcReWMKhWgCPkvAVHD
next build fallaba en tsc: defaultTile no tenia la propiedad opcional
image, asi que la union de tipos entre tileContent[handle] y defaultTile
perdia esa propiedad y content.image no compilaba. Se extrae el tipo
TileContent y se anota defaultTile con el, igual que tileContent.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013dunTcReWMKhWgCPkvAVHD
El guard de "Validar navegación real del catálogo" en build.yaml seguia
pinneado a la implementacion vieja de navbar.tsx: greppeaba literalmente
"collection=Marvel", "category=Mochilas" y "currentCollection", que eran
exactamente los valores hardcodeados que PR #32 elimino por estar rotos
(ya no correspondian a ningun dato real desde que se quito
legacyProductMeta en PR #29). El guard quedo validando el bug, no la
correccion, y bloqueo el build post-merge de PR #32.
Ahora valida el comportamiento real nuevo: que el nav arma su arbol
desde datos reales (buildCategoryTree), que linkea por nombre real de
categoria (encodeURIComponent(root.name)) y que expone el item activo
(aria-current={isActive(...)}). El check de remontaje en
app/catalog/page.tsx tambien se actualizo: la key ya no incluye
":${section}" porque ese query param nunca se usaba (dead code
eliminado en PR #32).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013dunTcReWMKhWgCPkvAVHD
Sub-fase 2 de Etapa 2 (ADR 0005 seccion 6). Navbar y CategoryTiles usaban
arrays hardcodeados (Figuras/Peluches/Mochilas/Marvel) que ya no coinciden
con los nombres reales de categoria en Medusa desde que se elimino
legacyProductMeta (PR #29) -- todos esos links de navegacion quedaron
rotos silenciosamente (0 productos al filtrar).
- lib/headless: nuevo tipo Category + metodo getCategories() en
CatalogProvider (medusa via /store/product-categories, mock deriva de
su catalogo plano, shopify sin implementar como el resto de sus metodos).
- Navbar: arbol real de categorias (6 raices/21 nodos), dropdown de
hijos por raiz en desktop, lista anidada en el sheet movil. Colecciones
del sheet ahora se derivan de los productos reales en vez de un array
hardcodeado con 2 nombres obsoletos (Marvel/Frozen vs. los reales
Universo Marvel/Reino de Frozen).
- CategoryTiles: tiles de home por categoria raiz real; imagen solo donde
ya existe asset, gradiente honesto donde no (no hay fotografia para las
6 raices todavia, deuda explicita del ADR).
- catalog-client/catalog page: cuando el query param `category` es una
raiz con hijos, se expande a los nombres reales de sus hijos para que
el filtro (igualdad exacta de producto.category) siga funcionando --
sin esto, tiles/nav apuntando a una raiz mostrarian 0 productos porque
ningun producto tiene la raiz asignada directamente.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013dunTcReWMKhWgCPkvAVHD
- Trivy IaC escaneaba solo el patch aislado de Kustomize
(workloads/ecommerce/frontend/patches/deployment.yaml), no lo que
realmente corre en el clúster. Eso generaba falsos positivos en todo
lo heredado del base remoto de platform-templates (runAsNonRoot,
seccompProfile) porque el patch nunca los repite. Se instala kubectl,
se renderiza el kustomization completo y se escanea ese output.
- readOnlyRootFilesystem pasa de false a true en el contenedor del
frontend: revisando next.config.ts (optimización de imágenes) y
app/api/revalidate/route.ts (revalidateTag sin cache handler custom),
la imagen sí escribe a ./.next/cache en runtime. Se agregan volúmenes
emptyDir para .next/cache y /tmp.
El default de Trivy (5m) dejó de alcanzar para escanear la imagen del
frontend, igual que ya le había pasado a commerce-backend el
2026-08-16 ("context deadline exceeded" ~4m45s, justo tras terminar
el docker build). Se aplica el mismo fix ya validado en
build-medusa.yaml: --timeout 15m0s en ambos scans de imagen (CRITICAL
y HIGH), y se sube timeout-minutes del job de 20 a 30 para que quepan.
Los 17 productos legacy ya tienen category, collection, brand, ageRange,
imagenes y featured/isNew reales en Medusa (migrados en fix #2/#2b de
Etapa 2 sub-fase 1), por lo que el fallback hardcodeado en mapProduct()
ya no aporta nada y solo duplica datos que pueden desincronizarse.
+categories sin .* no expande la relacion (Medusa la omite en
silencio del response); +categories.* si trae los sub-campos.
Confirmado en vivo via kubectl exec antes de este commit: demo
con categoria real asignada ahora la trae, legacy sigue en []
(pendiente fix#2, fuera de alcance de este cambio).
/store/products descarta en silencio la relacion +categories aunque
/admin/products si la expone (bug real, no teorico, confirmado en la
auditoria de Fase 5 con las 21 categorias y 9 productos [DEMO]
creados en Etapa 1 -- todos caian a "Variedades" pese a tener
categoria real asignada). Middleware global sobre req.allowed, patron
oficial de Medusa v2 para ampliar campos de una ruta core restringida.
Etapa 2, sub-fase 1 (categorias/colecciones reales) de
docs/decisiones/0005-rediseno-storefront.md.
Fase 5 Etapa 1: auditoria del catalogo actual (categorias/colecciones
de Medusa sin uso real, nav hardcodeado, bug de +categories en Store
API), decision de evolucionar el stack en vez de reescribirlo,
patrones UX de Amazon/eBay/Walmart y taxonomia de 21 categorias ya
creada en Medusa junto con 9 productos [DEMO] validados en vivo en
shop.cruzcloud.net como linea base pre-rediseno.
Con el runner corriendo 1 job a la vez (capacity: 1), cuando 2+
pipelines comparten el mismo commit disparador, el primero en terminar
deja su propio commit de GitOps en main -- los siguientes veian 'main
ya no es igual a mi commit' y se saltaban su propia promocion, aunque
era seguro promocionar. Visto en vivo: Frontend construyo, escaneo y
firmo v1.0.119 pero el manifiesto seguia en v1.0.104 (Promocion GitOps:
false). Fix en los 3 workflows: git merge-base --is-ancestor en vez de
comparacion exacta de SHA. Bump manual de frontend a v1.0.119 (la
version que ya estaba lista, sin desplegar). Documentado el limite
conocido de este fix en el playbook de la migracion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Los 3 pipelines (build.yaml, build-medusa.yaml, deploy-docs.yaml)
todavia apuntaban a las rutas viejas de deployment.yaml (borradas al
migrar cada app a un overlay de platform-templates) para el paso de
auto-actualizacion GitOps. Se rompio en vivo en el primer build real
post-migracion (docs-portal v1.0.117): la imagen se construyo, escaneo
y firmo bien, pero el ultimo paso (sed sobre el manifest) fallo con
'No such file or directory'. Se corrigen las 3 rutas a
patches/deployment.yaml, y se bumpea a mano el tag de docs-portal a
v1.0.117 (la imagen ya existe en el registry, firmada y verificada,
no hace falta reconstruirla).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Nueva seccion 'Templates' con la pagina principal de platform-templates
(diagrama Mermaid del flujo codigo->produccion, los 3 arquetipos, el
hallazgo real de Gitea sobre uses: cross-repo). Nuevo playbook de la
migracion de docs-portal/frontend/medusa, con los 3 bugs reales
encontrados en produccion (runAsNonRoot sin imagen no-root, capabilities
rompiendo el chown de nginx, runAsNonRoot sin runAsUser numerico) y los
2 gotchas de Kustomize (rename via patch, anular un campo heredado).
Nota: el directorio se llama docs/plantillas/ y no docs/templates/ --
MkDocs excluye silenciosamente cualquier carpeta llamada 'templates'
del build (confirmado en vivo con mkdocs build --strict, sin ningun
error/warning que lo explique, solo un INFO 'excluded from built site').
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Reemplaza medusa.yaml (Deployment+Service en un solo archivo) por un
overlay de Kustomize sobre el base remoto de platform-templates
(workloads/ecommerce/commerce/medusa/). Agrega securityContext,
incluyendo runAsUser: 1000 explicito desde el arranque (confirmado con
'docker run ... id node' contra la imagen real antes de asumir el UID
-- ya se aprendio en la migracion de frontend que runAsNonRoot solo no
alcanza si el USER de la imagen es un nombre). El podAffinity del
workaround de MTU/PMTUD y los 2 initContainers (wait-for-postgres,
migrations) se preservan igual, son campos aditivos que no forman
parte del base. El Ingress generico del base se excluye via
$patch:delete -- medusa no tiene Ingress propio, comparte
commerce-ingress. Validado con kubectl kustomize contra el arbol
completo de workloads/ecommerce antes de aplicar.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Detectado en vivo: el pod nuevo fallaba con CreateContainerConfigError
-- 'container has runAsNonRoot and image has non-numeric user (nextjs),
cannot verify user is non-root'. El Dockerfile define el usuario por
nombre (useradd ... nextjs), Kubernetes no puede confirmar el UID real
sin arrancar el contenedor. Se agrega runAsUser: 1001 explicito
(coincide con el --uid 1001 del useradd real del Dockerfile). Los 2
pods viejos siguieron sirviendo trafico sin interrupcion mientras tanto.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Reemplaza frontend.yaml + patches/frontend-resources.yaml por un overlay
de Kustomize sobre el base remoto de platform-templates
(workloads/ecommerce/frontend/). Agrega readinessProbe/livenessProbe
(no tenia ninguna, apuntando a /api/health que ya expone el propio
Dockerfile) y securityContext (capabilities NET_BIND_SERVICE agregada
de vuelta por el puerto 80 privilegiado -- frontend ya corre no-root de
antes, sin el problema de chown que tuvo docs-portal). El Ingress
generico del base se excluye via $patch:delete -- frontend comparte
ecommerce-ingress con catalog-api-svc, no le corresponde un Ingress
propio. Validado con kubectl kustomize contra el arbol completo de
workloads/ecommerce antes de aplicar.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Detectado en vivo: el pod nuevo crasheaba con 'chown(...) Operation not
permitted' -- nginx:1.27-alpine necesita CAP_CHOWN en su propio
entrypoint. drop:[ALL] + add:[NET_BIND_SERVICE] no alcanzaba. Se saca
la restriccion de capabilities entera para este container (capabilities:
null en el patch, unica forma real de anular el drop:[ALL] heredado del
base -- omitir el campo u objeto vacio no alcanzan, confirmado en vivo).
No es peor que el estado actual en produccion; se resuelve del todo con
nginx-unprivileged.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Reemplaza los manifiestos sueltos (deployment/service/ingress) por un
overlay de Kustomize sobre el base remoto de platform-templates. Agrega
readinessProbe/livenessProbe (no tenia ninguna) y securityContext
(ajustado: runAsNonRoot=false + NET_BIND_SERVICE, porque la imagen
actual de docs-portal corre como root en el puerto 80 -- se resuelve
del todo cuando docs-portal adopte dockerfiles/static-nginx.Dockerfile
de platform-templates, cambio de imagen aparte). Validado con kubectl
kustomize contra el manifiesto real desplegado antes de aplicar.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Nuevo playbook del blackhole MTU cross-node (diagnostico + fix + validacion).
Actualiza el playbook de contencion de recursos Gitea/runner de 'pendiente'
a 'aplicado'. Actualiza cosign.md: firma por digest ya implementada, Rekor
como decision tomada (no TODO). Corrige 2 links que quedaron rotos al
renombrar la seccion de limitaciones (validado con mkdocs build --strict).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Confirmado empiricamente el limite real (1420 OK / 1450 blackhole),
aplicado y validado el fix (flannel.1 mtu 1400 + rollout restart de
ecommerce). El podAffinity de medusa.yaml se deja como red de contencion,
ya no como requisito.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Checklist para revisar en cada bump de @medusajs/* si las excepciones
documentadas (esbuild/vite via admin-sdk) siguen vigentes o deben
removerse. Evita que una excepcion se vuelva permanente por olvido.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
Los 3 pipelines (frontend, commerce-backend, docs-portal) ahora capturan
el digest real del docker push y lo usan como referencia para cosign
sign/verify, en vez del tag mutable :VERSION. Documenta ademas la
decision de mantener Rekor deshabilitado (registry privado, evita
dependencia de red saliente hacia rekor.sigstore.dev via el tunel).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
El fix de CVE-2026-59873 (rm -rf npm entero, mergeado en
fix/devsecops-medusa-cve-remediation) rompio en produccion el init
container "migrations" de workloads/ecommerce/commerce/medusa.yaml,
que corria "npx medusa db:migrate" contra la misma imagen. Detectado
revisando el estado real del cluster despues del deploy (kubectl get
pods): medusa-deploy quedo atascado en Init:CrashLoopBackOff con
"npx: executable file not found in $PATH".
Se probo primero borrar solo el tar vendorizado dentro de npm (en vez
de npm entero) para no tocar npx -- no alcanza: el propio npx depende
de ese mismo tar internamente (pacote/arborist), asi que sigue roto
igual ("Cannot find module 'tar'"), confirmado localmente.
Fix real: mantener el rm -rf de npm completo (sin CVE), y cambiar el
init container para invocar el CLI de Medusa directo con node, sin
pasar por npx -- mismo patron que ya usa el CMD de runtime de esta
misma imagen. Verificado localmente: build OK, "node .../cli/dist/index.js
db:migrate --help" funciona sin npm/npx presentes, Trivy en exit 0.
5 playbooks nuevos en docs/playbooks/ del portal:
- SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve.
- Anchors YAML no soportados por el parser de Gitea Actions, incluido
el diagnostico equivocado inicial (indentacion) que se corrigio
despues -- documentado con honestidad como leccion de proceso.
- Docker-in-Docker vs sibling containers, y por que Semgrep corre
nativo en un venv en vez de como container aparte.
- El SNI/realm HTTP del tunel rompiendo especificamente a Cosign
(docker push funcionaba, cosign sign no), conectado con el
incidente ya documentado del 504 -- misma capa de ingress, dos
fallas distintas.
- Contencion de recursos entre Gitea y su runner: causa raiz real
confirmada en vivo durante esta misma sesion (ambos contenedores
reiniciados a mitad de un build, sin limites de CPU/memoria reales),
con fix propuesto (ver rama fix/gitea-runner-resource-limits en
scripts/) pendiente de aplicacion manual.
Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo
de contencion, cerrando el loop que habia quedado abierto ahi.
cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras"
con los dos TODO reales -- firma por tag en vez de digest (con el
warning real de Cosign visto en los logs) y transparency log de Rekor
deshabilitado, explicado en simple.
index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de
Semgrep por stack), diagrama generalizado para reflejar que es el
mismo patron en los 3 workflows, y "que falta" actualizado.
Validado con mkdocs build --strict (0 errores, 0 warnings) antes de
commitear.
Run 144 de deploy-docs.yaml encontro un CVE CRITICAL real
(CVE-2026-31789, heap buffer overflow en OpenSSL) en libssl3/libcrypto3
de la base nginx:1.27-alpine, con fix ya publicado por Alpine
(3.3.3-r0 -> 3.3.7-r0). Se agrega apk upgrade puntual de esos dos
paquetes en el Dockerfile. Verificado localmente: docker build OK +
Trivy real en exit 0 (0 vulnerabilidades).
Run 138 de build-medusa.yaml encontro 5 CVE CRITICAL reales (no falsos
positivos) al escanear la imagen con Trivy:
Remediados en el Dockerfile (verificado localmente con Trivy real,
exit 0):
- CVE-2026-33845 / CVE-2026-42010 (libgnutls30, base Debian): apt
upgrade puntual del paquete.
- CVE-2026-59873 (tar vendorizado dentro del npm CLI global de la
imagen base node:*-slim): se elimina /usr/local/lib/node_modules/npm
completo -- no es una dependencia real del proyecto (el CMD nunca
invoca npm en runtime), asi que no hay nada que "actualizar".
Documentados como excepcion en workloads/commerce-backend/.trivyignore
(con --ignorefile solo en este workflow, no afecta a build.yaml ni
deploy-docs.yaml):
- CVE-2024-24790 / CVE-2025-68121 (Go stdlib embebido en el binario de
esbuild que trae [email protected], dependencia interna de
@medusajs/admin-sdk). Se probo el fix real (override de esbuild a
una version mas nueva) y rompio el build del admin panel --
[email protected] fija "esbuild: ^0.21.3" como dependencia directa, y
esbuild >=0.24 cambio el manejo de targets de transpilacion que vite
5 espera. No existe un patch dentro de la serie 0.21.x con un Go
toolchain mas nuevo. Requiere subir @medusajs/admin-sdk/vite en un
cambio aparte, fuera de alcance de este pipeline de seguridad.
El run 136 (primer intento de este pipeline, recien mergeado a main)
fallo en el paso "Escanear imagen (Trivy) - CRITICAL bloquea" con
"context deadline exceeded" a los 4m52s -- no una CVE, un timeout de
herramienta. Causa: el default de Trivy (5m) no alcanza para escanear
(vuln + secret scan de la imagen completa) el node_modules de Medusa
v2, mas pesado que el del frontend. Se agrega --timeout 15m0s a ambos
escaneos de imagen (CRITICAL y HIGH).
Preventivo: mismo comando que en commerce-backend, donde se vio en
vivo que el timeout default de Trivy (5m) no alcanza en este host bajo
carga ("context deadline exceeded" a los 4m52s, run 136). Ver
fix/devsecops-trivy-timeout para el fix real en commerce-backend.
Agrega a deploy-docs.yaml las mismas 6 etapas ya validadas en
build.yaml (run 134, frontend): Gitleaks, Trivy IaC, Semgrep (SAST),
Trivy imagen, Syft (SBOM) y Cosign (firma + verificación).
Adaptaciones de stack:
- Trivy IaC escanea todo workloads/docs-portal/ (varios manifiestos
K8s separados: deployment/service/ingress), no un MANIFEST_FILE
único como el frontend.
- Semgrep usa p/python + p/security-audit (no hay TypeScript/React
acá) -- docs-portal no tiene código de aplicación propio hoy (solo
Markdown + config de MkDocs), así que 0 hallazgos es el resultado
esperado; el stage queda listo para el día que se agregue un
hook/plugin en Python.
Mismos umbrales que el resto: Trivy bloquea en CRITICAL, Semgrep en
modo auditoría. Reutiliza el mismo par de llaves Cosign (secrets ya
existentes a nivel de repo); se agrega workloads/docs-portal/cosign.pub.
Agrega a build-medusa.yaml las mismas 6 etapas ya validadas en
build.yaml (run 134, frontend): Gitleaks, Trivy IaC, Semgrep (SAST),
Trivy imagen, Syft (SBOM) y Cosign (firma + verificación).
Ruleset de Semgrep adaptado: p/typescript + p/security-audit +
p/owasp-top-ten, sin p/react/p/nextjs -- commerce-backend es la API de
Medusa v2 (Node/TypeScript), no una app con SSR. Mismos umbrales que
el frontend: Trivy bloquea en CRITICAL, Semgrep en modo auditoría
(no bloquea todavía).
Reutiliza el mismo par de llaves Cosign del frontend (secrets ya
existentes a nivel de repo); se agrega workloads/commerce-backend/cosign.pub
con la misma llave pública para que la verificación quede local a
este workflow.
El fix fue en infraestructura (regla de ingress de cloudflared para
gitea.cruzcloud.net -> HTTPS con Origin Server Name correcto), no en
este repo -- este commit vacío es solo para generar un push nuevo y
validar el pipeline completo (incluida la firma de Cosign) de punta
a punta con las credenciales reales.
El runner de Gitea ejecuta cada job dentro de un contenedor propio que
habla con el daemon Docker del host (sibling containers) -- un
`docker run -v "\${{ github.workspace }}:/src"` desde ahí intenta
montar una ruta que solo existe DENTRO del contenedor del job, no en
el host real: "mkdir /workspace: read-only file system". Confirmado en
el run real (job 128, task_id 128) tras el fix del anchor.
Se valida localmente contra docker.gitea.com/runner-images:ubuntu-latest
(la misma imagen del runner real) que python3/pip3 están disponibles.
pip3 install directo falla por paquetes de apt sin metadata compatible
(PyJWT, "RECORD file not found") -- se usa un venv aislado en su lugar,
validado end-to-end con los dos steps extraídos del YAML real.