From 06ed58b6e7fc28e8eeaf02a01b35b7b0acf305d8 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Tue, 8 Sep 2026 18:34:54 +0200 Subject: [PATCH] ci(build): reuse the Docker build cache across runs --- .github/actions/build-test-push/action.yml | 6 +- .github/actions/build-test-push/build.sh | 75 ++++++++++++++++++++++ build/Dockerfile | 17 ++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/.github/actions/build-test-push/action.yml b/.github/actions/build-test-push/action.yml index 962adf3a..a96d5433 100644 --- a/.github/actions/build-test-push/action.yml +++ b/.github/actions/build-test-push/action.yml @@ -83,12 +83,16 @@ runs: INPUT_PLATFORM: ${{ inputs.platform }} INPUT_ALTERNATE_REPOSITORY: ${{ inputs.alternate_repository }} INPUT_DRY_RUN: ${{ inputs.dry_run }} + # Exporting the build cache needs a registry login. Forks run without + # credentials, so they import the cache but never export it. + INPUT_CACHE_WRITABLE: ${{ inputs.docker_hub_username != '' }} run: | .github/actions/build-test-push/build.sh \ --version "$INPUT_VERSION" \ --platform "$INPUT_PLATFORM" \ --alternate-repository "$INPUT_ALTERNATE_REPOSITORY" \ - --dry-run "$INPUT_DRY_RUN" + --dry-run "$INPUT_DRY_RUN" \ + --cache-writable "$INPUT_CACHE_WRITABLE" - name: Run integration tests if: inputs.skip_integrations_tests != 'true' diff --git a/.github/actions/build-test-push/build.sh b/.github/actions/build-test-push/build.sh index 1dca6034..34a3d4e3 100755 --- a/.github/actions/build-test-push/build.sh +++ b/.github/actions/build-test-push/build.sh @@ -12,6 +12,7 @@ version="" platform="" alternate_repository="" dry_run="" +cache_writable="" while [[ $# -gt 0 ]]; do case $1 in @@ -31,6 +32,10 @@ while [[ $# -gt 0 ]]; do dry_run="$2" shift 2 ;; + --cache-writable) + cache_writable="$2" + shift 2 + ;; *) echo "Unknown option $1" exit 1 @@ -44,11 +49,41 @@ echo echo "Gotenberg version: $version" echo "Target platform: $platform" +# The build cache lives under the canonical repository, captured before the +# alternate-repository override below. Pull requests build into "snapshot", so +# deriving the cache ref after the override would give them a cache namespace +# of their own and they would never import what main published, which is the +# population that benefits most. +cache_image="$DOCKER_REGISTRY/$DOCKER_REPOSITORY" + +# Layers are per-architecture, so each platform keeps its own cache manifest. +cache_platform="${platform//\//-}" + +# Layers running "apt-get upgrade" install whatever versions are current at +# build time, and the packages are deliberately not pinned. A persistent cache +# would turn those into hits and freeze security patches into a published +# image until debian:13-slim itself changes digest. Keying them on the ISO week +# bounds that staleness to seven days while leaving every build within a week +# free to reuse the cache. +apt_snapshot="$(date -u +%G-W%V)" + +# Only a build that is not redirected to an alternate repository writes the +# cache, so a pull request cannot make its own state the baseline for main. +# Reading stays enabled everywhere, including forks, since the cache ref is +# public and needs no credentials. +cache_to_enabled="false" +if [ "$cache_writable" = "true" ] && [ -z "$alternate_repository" ]; then + cache_to_enabled="true" +fi + if [ -n "$alternate_repository" ]; then DOCKER_REPOSITORY=$alternate_repository echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY" fi +echo "Build cache: $cache_image:buildcache--$cache_platform (write: $cache_to_enabled)" +echo "APT snapshot: $apt_snapshot" + if [ "$dry_run" = "true" ]; then echo "🚧 Dry run" fi @@ -189,12 +224,36 @@ join() { echo "$*" } +# cache_flags echoes the buildx cache arguments for a build target. Each target +# keeps its own manifest so that the Chromium and LibreOffice variants, which +# branch from common-stage rather than from each other, do not overwrite one +# another's entry. +# +# mode=max exports intermediate stages too, not just the final layers, which is +# what makes the expensive apt and jlink stages reusable. type=registry, not +# type=gha: the GitHub Actions cache is capped at 10 GB per repository and is +# already carrying the Go and golangci-lint caches that the lint and test jobs +# depend on. Multi-GB image layers across five platforms would evict them. +cache_flags() { + local target="$1" + local ref="$cache_image:buildcache-$target-$cache_platform" + local flags="--cache-from type=registry,ref=$ref" + + if [ "$cache_to_enabled" = "true" ]; then + flags="$flags --cache-to type=registry,ref=$ref,mode=max" + fi + + echo "$flags" +} + no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version" # Full variant. cmd="docker buildx build \ --target gotenberg \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg) \ --platform $platform \ --load \ ${tags_flags[*]} \ @@ -207,6 +266,8 @@ run_cmd "$cmd" cmd="docker buildx build \ --target gotenberg-chromium \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-chromium) \ --platform $platform \ --load \ ${tags_chromium_flags[*]} \ @@ -218,6 +279,8 @@ run_cmd "$cmd" cmd="docker buildx build \ --target gotenberg-libreoffice \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-libreoffice) \ --platform $platform \ --load \ ${tags_libreoffice_flags[*]} \ @@ -230,6 +293,8 @@ if [ "$platform" = "linux/amd64" ]; then cmd="docker buildx build \ --target gotenberg-cloudrun \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-cloudrun) \ --platform $platform \ --load \ ${tags_cloud_run_flags[*]} \ @@ -240,6 +305,8 @@ if [ "$platform" = "linux/amd64" ]; then cmd="docker buildx build \ --target gotenberg-cloudrun-chromium \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-cloudrun-chromium) \ --platform $platform \ --load \ ${tags_cloud_run_chromium_flags[*]} \ @@ -250,6 +317,8 @@ if [ "$platform" = "linux/amd64" ]; then cmd="docker buildx build \ --target gotenberg-cloudrun-libreoffice \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-cloudrun-libreoffice) \ --platform $platform \ --load \ ${tags_cloud_run_libreoffice_flags[*]} \ @@ -263,6 +332,8 @@ if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then cmd="docker buildx build \ --target gotenberg-aws-lambda \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-aws-lambda) \ --platform $platform \ --load \ ${tags_aws_lambda_flags[*]} \ @@ -273,6 +344,8 @@ if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then cmd="docker buildx build \ --target gotenberg-aws-lambda-chromium \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-aws-lambda-chromium) \ --platform $platform \ --load \ ${tags_aws_lambda_chromium_flags[*]} \ @@ -283,6 +356,8 @@ if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then cmd="docker buildx build \ --target gotenberg-aws-lambda-libreoffice \ --build-arg GOTENBERG_VERSION=$version \ + --build-arg APT_SNAPSHOT=$apt_snapshot \ + $(cache_flags gotenberg-aws-lambda-libreoffice) \ --platform $platform \ --load \ ${tags_aws_lambda_libreoffice_flags[*]} \ diff --git a/build/Dockerfile b/build/Dockerfile index 6fa57a9f..148c0f1f 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -59,7 +59,14 @@ RUN go build -o gotenberg -ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/ # ---------------------------------------------- FROM debian:13-slim AS custom-jre-stage -RUN apt-get update -qq \ +# APT_SNAPSHOT busts every layer below it when CI rotates the value, weekly. +# Without it a persistent build cache turns the unpinned "apt-get upgrade" into +# a cache hit and the published image keeps shipping the package versions that +# were current when the cache was first populated. +ARG APT_SNAPSHOT="" + +RUN echo "apt snapshot: $APT_SNAPSHOT" \ + && apt-get update -qq \ && apt-get upgrade -yqq \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends default-jdk-headless binutils @@ -114,9 +121,15 @@ FROM base-image-stage AS common-stage ARG GOTENBERG_USER_GID=1001 ARG GOTENBERG_USER_UID=1001 +# See the note on APT_SNAPSHOT in custom-jre-stage. Declaring it here covers +# every "apt-get upgrade" in the gotenberg, gotenberg-chromium and +# gotenberg-libreoffice targets too, since all three branch from this stage. +ARG APT_SNAPSHOT="" + # Create a non-root user. # All processes in the Docker container will run with this dedicated user. -RUN groupadd --gid "$GOTENBERG_USER_GID" gotenberg \ +RUN echo "apt snapshot: $APT_SNAPSHOT" \ + && groupadd --gid "$GOTENBERG_USER_GID" gotenberg \ && useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg \ && mkdir /home/gotenberg \ && chown gotenberg: /home/gotenberg