diff --git a/.forgejo/workflows/build-base.yaml b/.forgejo/workflows/build-base.yaml index cf46a601..f7d39d4e 100644 --- a/.forgejo/workflows/build-base.yaml +++ b/.forgejo/workflows/build-base.yaml @@ -1,9 +1,13 @@ name: Build base image -# Path-filtered so this only fires when the base Dockerfile (or this -# workflow) changes — typically a wgrib2 / g2c version bump or apt-dep -# tweak. `workflow_dispatch` lets us rebuild on demand without touching -# the file (e.g. to refresh CVE patches in the apt layer). +# Path-filtered so it fires when the base Dockerfile (or this workflow) +# changes, weekly via cron so Debian security updates land without us +# remembering to bump anything, and on-demand via workflow_dispatch. +# +# The weekly cron passes CACHE_BUST= as a build-arg so the +# apt layers in Dockerfile.base re-execute (otherwise BuildKit's layer +# cache would short-circuit `apt-get update` on a deterministic input +# and silently skip security patches). on: push: branches: @@ -11,6 +15,11 @@ on: paths: - 'Dockerfile.base' - '.forgejo/workflows/build-base.yaml' + schedule: + # Sundays 06:00 UTC — runs on a quiet day-of-week so a long base + # rebuild doesn't collide with mid-week deploys, and matches the + # cadence that Debian security advisories typically aggregate at. + - cron: '0 6 * * 0' workflow_dispatch: env: @@ -106,12 +115,19 @@ jobs: run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" TAG="${{ steps.tag.outputs.tag }}" + # ISO year+week (e.g. 2026-W18). Same value all week, so a + # cron-triggered rebuild on day 1 vs day 7 reuses the layer + # cache; the next ISO week boundary forces apt-get update + + # install to re-execute and pull fresh security patches. + CACHE_BUST=$(date -u +%G-W%V) + echo "CACHE_BUST=${CACHE_BUST}" attempt=1 max_attempts=3 while : ; do if docker buildx build \ -f Dockerfile.base \ + --build-arg CACHE_BUST="${CACHE_BUST}" \ -t "${IMAGE}:${TAG}" \ -t "${IMAGE}:latest" \ --push \ diff --git a/Dockerfile.base b/Dockerfile.base index d033fcb7..ed0a8475 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -70,12 +70,21 @@ RUN wget -q --content-disposition "https://github.com/NOAA-EMC/wgrib2/archive/re # ---- Final runtime base ---- FROM ${RUNNER_IMAGE} AS base +# Weekly cron passes CACHE_BUST= so the apt layers below +# re-execute and pick up Debian security patches. Source-controlled +# pushes leave the value at its `none` default, so day-to-day base +# rebuilds (Dockerfile.base edits) don't pay the apt-update cost +# unless the week has rolled over. +ARG CACHE_BUST=none + # Runtime apt deps. cdo + gdal-bin are split into a second RUN so a # transient cdo install failure doesn't invalidate the smaller base -# layer above. +# layer above. Referencing `${CACHE_BUST}` in the RUN line changes the +# layer hash week-to-week without altering the install set. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ - rm -f /etc/apt/apt.conf.d/docker-clean \ + echo "cache-bust=${CACHE_BUST}" \ + && rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update \ && apt-get install -y --no-install-recommends \ libstdc++6 openssl libncurses6 locales ca-certificates snmp \ @@ -83,7 +92,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ - rm -f /etc/apt/apt.conf.d/docker-clean \ + echo "cache-bust=${CACHE_BUST}" \ + && rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update \ && apt-get install -y --no-install-recommends cdo gdal-bin