diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4c0848f..247917ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,14 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + # Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so + # the `apk upgrade` layer rebuilds at least once per calendar day, even + # when every other input to the layer is cached. See Dockerfile:115-122 + # for the rationale. + - name: Compute daily apk cache bust value + id: apk-bust + run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + - name: Build Docker image (validation only) uses: docker/build-push-action@v7 with: @@ -140,6 +148,8 @@ jobs: tags: sencho:pr-test cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }} - name: Scan image for vulnerabilities (Trivy) # Hard-fails the PR on any HIGH or CRITICAL finding that is not listed diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 415b584c..d94c4131 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -38,6 +38,15 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + # Computed once per job run. Feeds Dockerfile's APK_CACHE_BUST arg so + # the `apk upgrade` layer rebuilds at least once per calendar day, even + # when every other input to the layer is cached. See Dockerfile:115-122 + # for the rationale. Both the pre-publish scan build and the multi-arch + # push build below consume this so Trivy scans a fresh layer. + - name: Compute daily apk cache bust value + id: apk-bust + run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + - name: Log in to Docker Hub uses: docker/login-action@v4 with: @@ -88,6 +97,8 @@ jobs: platforms: linux/amd64 tags: localhost/sencho:release-scan cache-from: type=registry,ref=saelix/sencho:buildcache + build-args: | + APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }} - name: Re-scan release image for vulnerabilities (Trivy) # Gates the release on the same HIGH/CRITICAL policy as the PR scan. @@ -139,6 +150,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=saelix/sencho:buildcache cache-to: type=registry,ref=saelix/sencho:buildcache,mode=max + build-args: | + APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }} # SBOM + provenance attestations are embedded as OCI referrers on the # published image. Inspect with: docker buildx imagetools inspect sbom: true diff --git a/Dockerfile b/Dockerfile index 394728ee..3981d101 100644 --- a/Dockerfile +++ b/Dockerfile @@ -112,9 +112,19 @@ FROM node:22-alpine ARG DOCKER_VERSION=29.3.1 ARG COMPOSE_VERSION=v5.1.1 +# Daily cache-bust for the apk upgrade layer. CI passes the current date +# (YYYY-MM-DD) as a build-arg, so this RUN layer's hash changes at most +# once per calendar day. Without this, buildx reuses the cached layer +# indefinitely and a new Alpine package fix (e.g. an openssl CVE patched +# upstream in alpine 3.23) sits behind the stale cache until an unrelated +# change invalidates this line by coincidence. Default value lets local +# developers build without the arg; production CI always sets it. +ARG APK_CACHE_BUST=unset + # Upgrade all Alpine system packages, install runtime deps, then fetch Docker # CLI + Compose plugin from official static binaries. -RUN apk upgrade --no-cache && \ +RUN echo "apk cache bust: ${APK_CACHE_BUST}" && \ + apk upgrade --no-cache && \ apk add --no-cache bash su-exec curl && \ ARCH=$(uname -m) && \ curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz" \