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.
Gitea descartaba el workflow completo en TODOS los eventos (push y
pull_request), desde el primer commit de gitleaks: "unknown on type:
&yaml.Node{...Value:"frontend_paths"...}". El YAML era válido (pyyaml
lo parsea sin problema, resolviendo el alias como cualquier parser
estándar) pero el parser propio de Gitea Actions para el bloque "on:"
no resuelve &anchor/*alias antes de inspeccionar el tipo de nodo.
Confirmado en vivo: tras mergear #19 a main, deploy-docs.yaml corrió
normal (sin anchors) pero build.yaml no generó ningún action_run.
Fix: paths duplicados literalmente entre push: y pull_request:, sin
anchor. Válido con parser YAML + bash -n en los 20 steps.
El python3 -c "..." embebido dentro del run: | tenía el código pegado
a columna 0, por debajo de la indentación del bloque YAML -- eso corta
el block scalar ahí mismo y Gitea terminaba ignorando el workflow
completo ("could not find expected ':'"), silenciosamente, desde el
commit de Semgrep.
Validado con un parser YAML real (no solo con builds de Docker) y
bash -n sobre los 20 steps del archivo.
docs/devsecops/index.md documenta el pipeline completo (job gitleaks +
job build) con un diagrama Mermaid del orden real de ejecución, tabla
de qué bloquea vs qué solo informa, y qué pasa si cada herramienta
falla. Nav actualizado: "Resumen" como landing page de la sección
DevSecOps.
Con esto quedan las 5 herramientas de la Fase 2 (Gitleaks, Trivy,
Semgrep, Syft, Cosign) integradas y documentadas para
workloads/ecommerce, listas para replicar a commerce-backend y
docs-portal en una próxima vuelta.
Firma la imagen (solo el tag versionado, no :latest) después del push
exitoso, usando la llave privada desde secrets de Gitea Actions
(COSIGN_PRIVATE_KEY/COSIGN_PASSWORD, nunca en el repo ni en disco).
Verifica la firma como smoke test en el mismo pipeline contra
workloads/ecommerce/cosign.pub (pública, commiteada a propósito).
Firma solo con el par de llaves propio, sin publicar en el
transparency log público de Sigstore (--use-signing-config=false
--tlog-upload=false / --insecure-ignore-tlog=true) — registry privado,
no tiene sentido esa fuga de metadata.
Par de llaves generado y validado end-to-end (firma + verificación,
incluyendo que verificar con la llave incorrecta falla como se espera)
contra un registry local descartable antes de tocar nada real. Docs en
docs/devsecops/cosign.md, incluyendo qué falta para que un admission
controller verifique esto en el cluster (no implementado todavía).
Corre después del gate de CRITICAL de Trivy, sobre la imagen ya
construida. Genera CycloneDX y SPDX, publicados como artifact
sbom-v1.0.X en cada build.
Validado contra la imagen real: 3528 componentes CycloneDX / 228
paquetes SPDX. Documenta en docs/devsecops/sbom.md el propósito
(trazabilidad tipo "log4shell"), diferencia entre formatos, y un
hallazgo real (yarn empaquetado sin usarse, mismo patrón que el npm
sacado en el fix de Trivy).
Escanea workloads/ecommerce con rulesets públicos del registro de
Semgrep (p/typescript, p/react, p/nextjs, p/security-audit) vía la
imagen oficial semgrep/semgrep:1.173.0. Sin --error a propósito: siempre
termina en exit 0, solo reporta — primera vuelta para revisar juntos qué
reglas deberían pasar a bloquear más adelante.
Validado contra el código real: 0 hallazgos en 91 reglas / 72 archivos.
Documenta la herramienta en docs/devsecops/sast.md, incluyendo la
diferencia con gitleaks/trivy y qué significa (y qué no) un scan limpio.
trivy config sobre frontend.yaml (CRITICAL/HIGH/MEDIUM, informativo) y
trivy image sobre la imagen recién construida (CRITICAL bloquea con
--ignore-unfixed, HIGH solo informa), entre build (push:false, load:true)
y el push real al registry.
Fix real encontrado al validar contra la imagen real: el stage runner
heredaba npm/npx/corepack completos de la imagen base de Node sin
necesitarlos en runtime, trayendo CVE-2026-59873 (CRITICAL, node-tar
empaquetado en npm). Sacarlos del stage final baja CRITICAL de 1 a 0 y
HIGH fixable de 21 a 15 (las que quedan son de la propia app, ej. next
desactualizado, documentadas como pendiente sin bloquear).
Documenta ambos scans en docs/devsecops/trivy.md con los hallazgos
reales de este repo.
Corre en push y pull_request, antes del job build (needs: gitleaks).
Si detecta un secreto, exit-code=1 detiene el pipeline antes de
construir la imagen. Scan histórico completo del repo (249 commits)
confirmado limpio, corrido aparte de forma manual.
Documenta el step en docs/devsecops/gitleaks.md: qué es secret
scanning, por qué corre antes del build, cómo leer un hallazgo y
cómo manejar falsos positivos con allowlist.
mkdocs build --strict aborta ante CUALQUIER WARNING, no solo ante links
rotos. El plugin, al no tener .git real en el contexto de build
(workloads/docs-portal/, sin .git de la raíz del repo), cae siempre en
fallback_to_build_date y emite un WARNING por página — suficiente para
tumbar el build bajo --strict (confirmado en run 96, job 108: 33
warnings, todos de este plugin, cero links rotos).
Se deshabilita el plugin (comentado en mkdocs.yml y requirements.txt,
con TODO fase 2: mover el build a la raíz del repo + fetch-depth:0 para
darle historial real) y se revierte la instalación de git en el
Dockerfile, que ya no hace falta. index.md actualizado para no prometer
fechas reales que hoy no se muestran.
mkdocs build --strict (run 95, job 107) falló: el link a '#deuda-técnica-pendiente'
no coincide con el anchor real que genera mkdocs, porque el slugify por
defecto de Python-Markdown normaliza tildes (NFKD + strip de combining marks)
antes de generar el id del heading. El heading '## Deuda técnica pendiente'
genera 'deuda-tecnica-pendiente' (sin tilde), no 'deuda-técnica-pendiente'.
El plugin depende de GitPython, que necesita el binario git para
importarse (no solo para leer fechas) — sin él, mkdocs build --strict
falla en el import del plugin antes de llegar al fallback_to_build_date
configurado en mkdocs.yml.
Detectado en el primer intento real de build en Gitea Actions (run 94,
job 106).
Dispara el primer build real del portal via .gitea/workflows/deploy-docs.yaml
ahora que el Deployment, Service, Ingress y el secret de registry ya existen
en el namespace docs-portal.
Agrega lo que faltaba para desplegar workloads/docs-portal/ (ya existente
sin commitear): deployment/service/ingress + kustomization siguiendo el
patrón de workloads/nginx, Applications de workload y gobernanza
separadas, y el workflow de Gitea Actions (build+push a Gitea Registry,
bump de versión en el manifiesto) siguiendo el mismo patrón que
build.yaml/build-medusa.yaml.
Tres incidentes resueltos en la sesión del 2026-08-12/13: 504 intermitente
por QUIC inestable en cloudflared, precios null por falta de region_id
en el Store API de Medusa v2 (se descarta la hipótesis inicial de
regiones COP duplicadas con evidencia directa en Postgres), e imágenes
en blanco por un proxy host mal configurado en Nginx Proxy Manager para
media.cruzcloud.net. Se agregan también dos notas de referencia en
known-issues.md: la topología Cloudflare Tunnel -> NPM -> k3d, y el
requisito de region_id explícito en StoreGetProductsParams.