diff --git a/README.md b/README.md index abe8bd4..5ab8e88 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,22 @@ Enabled by default (`deployment-labels: 'true'`). The same three values are stam > **Note:** labels are baked in at **build time**, so they are only applied on builds. Release (`v*`) and custom-tag runs that **retag** an existing image instead of rebuilding (see [Image tags](#image-tags--flux-image-automation)) do not get fresh labels — the retagged image keeps the labels from the branch build it was promoted from. This feature is independent of the annotations above; enable either, both, or neither. +### Fresh builds + +Every build checks all referenced base images for updates (`pull: true`). Floating +tags pick up new base digests; digest-pinned references remain pinned. Build caching +is still enabled by default when the base image and build inputs are unchanged. + +Set `docker-build-no-cache: 'true'` for scheduled package or virus-signature refreshes. +This re-executes Dockerfile steps and skips external cache import/export in both +single-arch and multi-arch builds. It increases build time and download traffic; +retag-only runs are unaffected. + +```yaml +with: + docker-build-no-cache: 'true' +``` + ### Multi-Arch Images Our recovery/failover regions have no ARM capacity, so images deployed there must ship both `linux/amd64` and `linux/arm64`. The action never cross-compiles or emulates: each architecture is built natively on its own runner, and the results are combined into one manifest list afterwards. @@ -262,6 +278,7 @@ Notes: | `multiarch-artifact-name` | Base name of the artifact carrying the per-architecture digests between the build and merge jobs (the architecture is appended) | `docker-digests` | | `multiarch-digests-path` | Directory holding the per-architecture digest files | `/tmp/gitops-action-digests` | | `docker-build-provenance` | Generate [provenance](https://docs.docker.com/build/attestations/slsa-provenance/) attestation for the build | `false` | +| `docker-build-no-cache` | Re-execute Dockerfile steps and skip external cache import/export. Base images are always checked for updates. | `false` | | `docker-disable-retagging` | Disables retagging of existing images and run a new build instead | `false` | | `deployment-annotations` | Stamp deployment-tracking annotations (`deploy.staffbase.com/*`) onto updated GitOps manifests. See [Deployment tracking annotations](#deployment-tracking-annotations) | `true` | | `deployment-domain` | Key namespace for deployment-tracking metadata. Used verbatim for annotation keys (`/...`) and reversed to reverse-DNS for label keys (`com.staffbase.deploy.*`) | `deploy.staffbase.com` | diff --git a/action.yml b/action.yml index 8773078..95dc7e9 100644 --- a/action.yml +++ b/action.yml @@ -55,6 +55,10 @@ inputs: description: "Generate provenance attestation for the build" required: false default: 'false' + docker-build-no-cache: + description: 'Re-execute Dockerfile steps without cached build results or external cache import/export. Base images are always checked for updates independently of this setting.' + required: false + default: 'false' docker-build-outputs: description: "Custom output destinations (e.g., type=registry,push=true,compression=zstd,force-compression=true). When set, this replaces the default push behavior - include push=true if pushing is desired." required: false @@ -169,6 +173,7 @@ runs: INPUT_MULTIARCH_MODE: ${{ inputs.multiarch-mode }} INPUT_DOCKER_BUILD_PLATFORMS: ${{ inputs.docker-build-platforms }} INPUT_DOCKER_BUILD_OUTPUTS: ${{ inputs.docker-build-outputs }} + INPUT_DOCKER_BUILD_NO_CACHE: ${{ inputs.docker-build-no-cache }} INPUT_DOCKER_REGISTRY: ${{ steps.preparation.outputs.primary_registry }} INPUT_DOCKER_IMAGE: ${{ inputs.docker-image }} INPUT_TAG_LIST: ${{ steps.preparation.outputs.tag_list }} @@ -214,8 +219,11 @@ runs: secrets: ${{ inputs.docker-build-secrets }} secret-files: ${{ inputs.docker-build-secret-files }} platforms: ${{ steps.build_config.outputs.platforms }} - cache-from: type=gha${{ steps.build_config.outputs.cache_suffix }} - cache-to: type=gha,mode=max${{ steps.build_config.outputs.cache_suffix }} + # Floating hardened base tags can receive patches without changing name. + pull: true + no-cache: ${{ inputs.docker-build-no-cache }} + cache-from: ${{ steps.build_config.outputs.cache_from }} + cache-to: ${{ steps.build_config.outputs.cache_to }} provenance: ${{ inputs.docker-build-provenance }} outputs: ${{ steps.build_config.outputs.build_outputs }} diff --git a/scripts/resolve-build-config.sh b/scripts/resolve-build-config.sh index 5827110..e75db84 100755 --- a/scripts/resolve-build-config.sh +++ b/scripts/resolve-build-config.sh @@ -9,9 +9,11 @@ # Required env vars: RUNNER_ARCH # Optional env vars: INPUT_MULTIARCH_MODE, INPUT_DOCKER_BUILD_PLATFORMS, # INPUT_DOCKER_BUILD_OUTPUTS, INPUT_TAG_LIST, INPUT_PUSH, -# INPUT_DOCKER_REGISTRY, INPUT_DOCKER_IMAGE +# INPUT_DOCKER_REGISTRY, INPUT_DOCKER_IMAGE, +# INPUT_DOCKER_BUILD_NO_CACHE # -# Outputs (via GITHUB_OUTPUT): arch, platforms, tags, build_outputs +# Outputs (via GITHUB_OUTPUT): arch, platforms, tags, build_outputs, +# cache_suffix, cache_from, cache_to SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh @@ -65,8 +67,21 @@ if [[ "$MODE" == "build" ]]; then CACHE_SUFFIX=",scope=${ARCH}" fi +CACHE_FROM="" +CACHE_TO="" +# Fresh scheduled rebuilds have no use for external cache transfers. +if [[ "${INPUT_DOCKER_BUILD_NO_CACHE:-false}" == "false" ]]; then + CACHE_FROM="type=gha${CACHE_SUFFIX}" + CACHE_TO="type=gha,mode=max${CACHE_SUFFIX}" +elif [[ "$INPUT_DOCKER_BUILD_NO_CACHE" != "true" ]]; then + log_error "docker-build-no-cache must be 'true' or 'false'." + exit 1 +fi + set_output "arch" "$ARCH" set_output "cache_suffix" "$CACHE_SUFFIX" +set_output "cache_from" "$CACHE_FROM" +set_output "cache_to" "$CACHE_TO" set_output "platforms" "$PLATFORMS" set_output "tags" "$TAGS" set_output "build_outputs" "$BUILD_OUTPUTS" diff --git a/tests/resolve-build-config.bats b/tests/resolve-build-config.bats index 339d0d6..70dcb4b 100644 --- a/tests/resolve-build-config.bats +++ b/tests/resolve-build-config.bats @@ -27,6 +27,8 @@ teardown() { assert_output_value "tags" "registry.example.com/private/my-service:dev-abcdef12" assert_output_value "build_outputs" "" assert_output_value "cache_suffix" "" + assert_output_value "cache_from" "type=gha" + assert_output_value "cache_to" "type=gha,mode=max" } @test "default mode keeps custom docker-build-outputs" { @@ -54,6 +56,8 @@ teardown() { assert_output_value "tags" "" assert_output_value "build_outputs" "type=image,name=registry.example.com/private/my-service,push-by-digest=true,name-canonical=true,push=true" assert_output_value "cache_suffix" ",scope=amd64" + assert_output_value "cache_from" "type=gha,scope=amd64" + assert_output_value "cache_to" "type=gha,mode=max,scope=amd64" } @test "build mode on ARM64 builds arm64 regardless of docker-build-platforms" { @@ -106,3 +110,32 @@ teardown() { assert_failure assert_output --partial "Invalid multiarch-mode" } + +@test "no-cache omits external cache in single-arch and native multi-arch builds" { + export INPUT_DOCKER_BUILD_NO_CACHE="true" + for mode in "" build; do + export INPUT_MULTIARCH_MODE="$mode" + for arch in X64 ARM64; do + export RUNNER_ARCH="$arch" + # assert_output_value reads the first match, so start each run clean + : > "$GITHUB_OUTPUT" + run "$SCRIPT" + assert_success + assert_output_value "cache_from" "" + assert_output_value "cache_to" "" + done + done +} + +@test "explicit false retains external cache and invalid no-cache values fail" { + export INPUT_DOCKER_BUILD_NO_CACHE="false" + run "$SCRIPT" + assert_success + assert_output_value "cache_from" "type=gha" + assert_output_value "cache_to" "type=gha,mode=max" + + export INPUT_DOCKER_BUILD_NO_CACHE="yes" + run "$SCRIPT" + assert_failure + assert_output --partial "docker-build-no-cache must be 'true' or 'false'" +}