From 8ca8a2dfc59fff0a4aaf909c58acc35d7dd27a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Matar=C3=A1n=20Barrio?= Date: Sun, 9 Aug 2026 17:23:20 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20un=20=C3=A1mbito=20de=20cach=C3=A9=20por?= =?UTF-8?q?=20imagen=20en=20el=20bake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docker/bake-action` recibía `*.cache-to=type=gha,mode=max`, un comodín sobre los tres targets. `type=gha` sin `scope` usa siempre el mismo, así que app, worker y proxy exportaban a la MISMA entrada de caché a la vez y la que perdía la carrera tumbaba el build entero: #25 [app] exporting to GitHub Actions Cache #25 ERROR: failed to reserve cache #27 [proxy] exporting to GitHub Actions Cache ... CANCELED Al depender de qué imagen termina antes, fallaba unas veces sí y otras no; nada que ver con el contenido de la rama. Los ámbitos pasan al propio `docker-bake.hcl`, uno por imagen, activados por la variable CACHE (vacía en local, donde no existe el servicio de Actions y buildx abortaría). `worker` lee además el ámbito de `app` —es `app` más ffmpeg— para no perder el ahorro de capas en un arranque en frío, pero escribe sólo en el suyo, que es lo que evita la colisión. Comprobado con `--print`: sin CACHE no se emite configuración de caché y el bake local sigue construyendo; con CACHE=gha cada target lleva su scope. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019hnntZxZHwjjdygeifjYWF --- .github/workflows/cd-main.yml | 5 ++--- .github/workflows/ci.yml | 6 ++++-- docker/docker-bake.hcl | 36 ++++++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cd-main.yml b/.github/workflows/cd-main.yml index 71ec09d..b2a949b 100644 --- a/.github/workflows/cd-main.yml +++ b/.github/workflows/cd-main.yml @@ -121,10 +121,9 @@ jobs: files: docker/docker-bake.hcl targets: default push: true - set: | - *.cache-from=type=gha - *.cache-to=type=gha,mode=max env: + # Un ámbito de caché por imagen (ver docker/docker-bake.hcl). + CACHE: gha REGISTRY: ghcr.io REPO: ${{ steps.meta.outputs.repo }} TAGS: ${{ steps.meta.outputs.sha_tag }},edge diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7090595..092c125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,8 +114,10 @@ jobs: targets: default push: false set: | - *.cache-from=type=gha - *.cache-to=type=gha,mode=max *.platform=linux/amd64 env: APP_VERSION: ${{ github.sha }} + # Los ámbitos de caché los declara cada target en docker-bake.hcl: uno + # por imagen. Con un `*.cache-to` comodín las tres compartían ámbito y + # se peleaban por reservar la misma entrada («failed to reserve cache»). + CACHE: gha diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl index d45cb42..585769e 100644 --- a/docker/docker-bake.hcl +++ b/docker/docker-bake.hcl @@ -22,6 +22,27 @@ variable "APP_VERSION" { default = "dev" } // sobreescribe PLATFORMS al publicar los manifiestos multi-arquitectura. variable "PLATFORMS" { default = "linux/amd64" } +// Caché de capas entre ejecuciones. Vacío en local —fuera de GitHub Actions no +// existe el servicio y buildx abortaría—; el CI la activa con CACHE=gha. +// +// Cada imagen usa su PROPIO ámbito, y ahí está el motivo de que esto no sea un +// `*.cache-to` a secas en el workflow: `type=gha` sin `scope` usa siempre el +// mismo, así que las tres exportaciones concurrentes se peleaban por reservar +// la misma entrada y la que perdía la carrera tumbaba el build entero con +// «failed to reserve cache». Al depender de qué imagen termina antes, fallaba +// unas veces sí y otras no. +variable "CACHE" { default = "" } + +function "cache_from" { + params = [scopes] + result = CACHE == "gha" ? [for s in scopes : "type=gha,scope=${s}"] : [] +} + +function "cache_to" { + params = [scope] + result = CACHE == "gha" ? ["type=gha,mode=max,scope=${scope}"] : [] +} + group "default" { targets = ["app", "worker", "proxy"] } @@ -43,15 +64,22 @@ target "_common" { } target "app" { - inherits = ["_common"] - target = "app" - tags = [for t in split(",", TAGS) : "${REGISTRY}/${REPO}/app:${t}"] + inherits = ["_common"] + target = "app" + tags = [for t in split(",", TAGS) : "${REGISTRY}/${REPO}/app:${t}"] + cache-from = cache_from(["app"]) + cache-to = cache_to("app") } target "worker" { inherits = ["_common"] target = "worker" tags = [for t in split(",", TAGS) : "${REGISTRY}/${REPO}/worker:${t}"] + // Lee también el ámbito de `app`: el worker es `app` más ffmpeg, así que en + // un arranque en frío se ahorra todas las capas de dependencias. Escribe sólo + // en el suyo, que es lo que evita la colisión. + cache-from = cache_from(["worker", "app"]) + cache-to = cache_to("worker") } // Otro Dockerfile (nada que ver con Node), pero el mismo contexto: necesita @@ -60,4 +88,6 @@ target "proxy" { inherits = ["_common"] dockerfile = "docker/Dockerfile.proxy" tags = [for t in split(",", TAGS) : "${REGISTRY}/${REPO}/proxy:${t}"] + cache-from = cache_from(["proxy"]) + cache-to = cache_to("proxy") }