Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tough-clouds-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"build-push-docker": minor
"ctf-build-image": minor
---

Adds buildkit-cache-dance to properly, persistently cache docker build layers in CI
8 changes: 6 additions & 2 deletions actions/build-push-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ multi-platform manifest from per-arch builds.
| `docker-push` | no | `true` | Push the built image. Set to `false` for a build-only (no push) run. |
| `tags` | no | `type=sha,prefix=pr=,event=pr` / `type=ref,event=tag` | Tag spec consumed by [docker/metadata-action](https://github.com/docker/metadata-action). |
| `allow-overwrites` | no | `true` | When `false`, the action fails before building if any computed tag already exists in ECR. Useful for pseudo-immutability on public ECRs (which don't support native immutability) or as a fast-fail guard on private immutable ECRs. Ignored when `docker-push` is `false`. |
| `docker-restore-cache` | no | `false` | Restore the Docker layer cache before building. |
| `docker-save-cache` | no | `false` | Save the Docker layer cache after building. |
| `cache-mode` | no | `auto` | Caching mode preset: `auto` (default: save on push/schedule, restore on PRs), `read-write`, `read-only`, `write-only`, or `off`. |
| `cache-map` | no | — | JSON string mapping cache mount paths for `buildkit-cache-dance` (e.g. `'{"go-mod-cache": "/go/pkg/mod"}'`). Passing a non-empty `cache-map` automatically enables `buildkit-cache-dance`. |
| `cache-dance` | no | — | _(Deprecated)_ Explicitly enable/disable `buildkit-cache-dance`. Inferred automatically if `cache-map` is set. |
| `cache-dance-cache-map` | no | — | _(Deprecated)_ Alias for `cache-map`. |
| `docker-restore-cache` | no | `false` | _(Deprecated override)_ Restore the Docker layer cache before building. |
| `docker-save-cache` | no | `false` | _(Deprecated override)_ Save the Docker layer cache after building. |
| `docker-build-cache-from` | no | GHA cache scoped to OS/arch | Override the cache source. Effective only when `docker-restore-cache` is `true`. |
| `docker-build-cache-to` | no | GHA cache scoped to OS/arch | Override the cache destination. Effective only when `docker-save-cache` is `true`. |
| `docker-attestations` | no | `true` | Generate SBOM and provenance attestations. See [Docker docs](https://docs.docker.com/build/ci/github-actions/attestations/). |
Expand Down
42 changes: 42 additions & 0 deletions actions/build-push-docker/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,29 @@ inputs:
Defaults to `true` for backwards compatibility. Ignored when inputs.docker-push is false.
required: false
default: "true"
cache-mode:
description: |
Caching mode preset: "auto" (default), "read-write", "read-only", "write-only", or "off".
required: false
default: "auto"
cache-map:
description: |
JSON string mapping cache mount paths/names for buildkit-cache-dance (e.g. '{"go-mod-cache": "/go/pkg/mod"}').
Passing a non-empty cache-map automatically enables buildkit-cache-dance.
See: https://github.com/reproducible-containers/buildkit-cache-dance
See: https://docs.docker.com/build/ci/github-actions/cache/#cache-mounts
required: false
default: ""
cache-dance:
description: |
Deprecated: Explicitly enable/disable buildkit-cache-dance. If unset, cache-dance is automatically enabled when cache-map is provided.
required: false
default: ""
cache-dance-cache-map:
description: |
Deprecated alias for cache-map.
required: false
default: ""
Comment on lines +139 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are "deprecated" but they were never released in the first place.


outputs:
docker-repository-name:
Expand Down Expand Up @@ -172,6 +195,16 @@ runs:
exit 1
fi

- name: Validate cache-dance configuration
id: validate-cache-dance
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
CACHE_DANCE: ${{ inputs.cache-dance }}
CACHE_MAP: ${{ inputs.cache-map || inputs.cache-dance-cache-map }}
run: |
"${ACTION_PATH}/scripts/validate-cache-dance-config.sh" >> "$GITHUB_OUTPUT"

- name: Check runner arch and platform compatibility
shell: bash
env:
Expand Down Expand Up @@ -361,6 +394,15 @@ runs:
echo "no-cache=false" | tee -a "${GITHUB_OUTPUT}"
echo "cache-from=${CACHE_FROM}" | tee -a "${GITHUB_OUTPUT}"

# Persist BuildKit RUN --mount=type=cache directories across GHA runs
# See: https://github.com/reproducible-containers/buildkit-cache-dance
# See: https://docs.docker.com/build/ci/github-actions/cache/#cache-mounts
- name: Cache Dance
if: ${{ steps.validate-cache-dance.outputs.cache-dance == 'true' }}
uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0
with:
cache-map: ${{ steps.validate-cache-dance.outputs.cache-map }}

- name: Build & push image
id: build-image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
Expand Down
58 changes: 58 additions & 0 deletions actions/build-push-docker/scripts/test-cache-dance-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash
Comment thread
kalverra marked this conversation as resolved.
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALIDATE_SCRIPT="${SCRIPT_DIR}/validate-cache-dance-config.sh"

echo "===== Testing build-push-docker cache dance configuration (Option 1) ====="

parse_val() {
local output="$1"
local key="$2"
echo "$output" | grep "^${key}=" | cut -d'=' -f2
}

echo "Test 1: Default - no cache-dance and no cache-map"
RES=$("$VALIDATE_SCRIPT" "" "")
DANCE=$(parse_val "$RES" "cache-dance")
[ "$DANCE" = "false" ] || (echo "FAIL: expected cache-dance=false, got $DANCE" && exit 1)
echo "Test 1 passed."

echo "Test 2: Implicit enable - cache-map provided without explicit cache-dance flag"
VALID_MAP='{"go-mod-cache": "/go/pkg/mod"}'
RES=$("$VALIDATE_SCRIPT" "" "$VALID_MAP")
DANCE=$(parse_val "$RES" "cache-dance")
MAP=$(parse_val "$RES" "cache-map")
[ "$DANCE" = "true" ] || (echo "FAIL: expected cache-dance=true, got $DANCE" && exit 1)
[ "$MAP" = "$VALID_MAP" ] || (echo "FAIL: expected cache-map match" && exit 1)
echo "Test 2 passed."

echo "Test 3: Invalid JSON in cache-map (should fail)"
if "$VALIDATE_SCRIPT" "" "invalid-json"; then
echo "FAIL: Expected failure when cache-map is invalid JSON"
exit 1
else
echo "Test 3 passed (failed as expected)."
fi

echo "Test 4: Explicit cache-dance=false overrides non-empty cache-map"
RES=$("$VALIDATE_SCRIPT" "false" "$VALID_MAP")
DANCE=$(parse_val "$RES" "cache-dance")
[ "$DANCE" = "false" ] || (echo "FAIL: expected cache-dance=false when explicitly disabled, got $DANCE" && exit 1)
echo "Test 4 passed."

echo "Test 5: Explicit cache-dance=true with missing cache-map (should fail)"
if "$VALIDATE_SCRIPT" "true" ""; then
echo "FAIL: Expected failure when cache-dance=true with missing cache-map"
exit 1
else
echo "Test 5 passed (failed as expected)."
fi

echo "Test 6: Backward compatibility with CACHE_DANCE_CACHE_MAP env var"
RES=$(CACHE_DANCE_CACHE_MAP="$VALID_MAP" "$VALIDATE_SCRIPT" "" "")
DANCE=$(parse_val "$RES" "cache-dance")
[ "$DANCE" = "true" ] || (echo "FAIL: expected cache-dance=true from env var fallback, got $DANCE" && exit 1)
echo "Test 6 passed."

echo "All cache dance configuration tests completed successfully."
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
set -e

# Validates cache-dance configuration inputs.
# Accepts positional arguments:
# $1: CACHE_DANCE ("true" | "false" | "")
# $2: CACHE_MAP (JSON string or path map)
# Or environment variables: CACHE_DANCE, CACHE_MAP, CACHE_DANCE_CACHE_MAP

EXPLICIT_DANCE="${1:-${CACHE_DANCE}}"
CACHE_MAP="${2:-${CACHE_MAP:-${CACHE_DANCE_CACHE_MAP}}}"

if [ -n "$CACHE_MAP" ]; then
if [ "$EXPLICIT_DANCE" = "false" ]; then
CACHE_DANCE="false"
else
CACHE_DANCE="true"
fi
else
if [ "$EXPLICIT_DANCE" = "true" ]; then
echo "::error::cache-map (or cache-dance-cache-map) input is required when cache-dance is true."
exit 1
fi
CACHE_DANCE="false"
fi

if [ "$CACHE_DANCE" = "true" ]; then
if ! echo "$CACHE_MAP" | jq . >/dev/null 2>&1; then
echo "::error::cache-map (or cache-dance-cache-map) must be valid JSON."
exit 1
fi
fi

echo "cache-dance=${CACHE_DANCE}"
echo "cache-map=${CACHE_MAP}"
6 changes: 6 additions & 0 deletions actions/ctf-build-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,9 @@ environments. If you find something broken, and need support reach out on
leave this alone.
- `platform` - Defaults to `linux/amd64`, use `linux/arm64` for arm-based
runners. Cross-platform builds are not supported.
- `cache-mode` - Caching preset: `auto` (default: restores cache for PRs, saves
cache for default branch push/schedule events), `read-write`, `read-only`,
`write-only`, or `off`.
- `cache-map` - JSON string mapping cache mount paths for `buildkit-cache-dance`
(e.g. `'{"go-mod-cache": "/go/pkg/mod"}'`). Passing a non-empty `cache-map`
automatically enables `buildkit-cache-dance`.
92 changes: 64 additions & 28 deletions actions/ctf-build-image/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,47 @@ inputs:
on runners with sufficient disk (e.g. RunsOn with 100GB+) to save ~30-60s.
default: "true"

cache-mode:
required: false
description: |
Caching mode preset: "auto" (default), "read-write", "read-only", "write-only", or "off".
If unset, defaults to "auto" (restores cache for PRs, saves cache for default branch push/schedule events).
See: https://docs.docker.com/build/ci/github-actions/cache/
default: "auto"

cache-map:
required: false
description: |
JSON string mapping cache mount paths/names for buildkit-cache-dance (e.g. '{"go-mod-cache": "/go/pkg/mod"}').
Passing a non-empty cache-map automatically enables buildkit-cache-dance.
See: https://github.com/reproducible-containers/buildkit-cache-dance
See: https://docs.docker.com/build/ci/github-actions/cache/#cache-mounts
default: ""

docker-restore-cache:
required: false
description: |
Deprecated explicit override for docker restore cache ("true" or "false").
default: ""

docker-save-cache:
required: false
description: |
Deprecated explicit override for docker save cache ("true" or "false").
default: ""

cache-dance:
required: false
description: |
Deprecated: Explicitly enable/disable buildkit-cache-dance.
default: ""

cache-dance-cache-map:
required: false
description: |
Deprecated alias for cache-map.
default: ""

outputs:
docker-image-sha-digest-amd64:
description: "Docker image SHA digest for platform: amd64"
Expand All @@ -148,40 +189,37 @@ runs:
- name: Process plugin manifest overrides (public)
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
PLUGIN_OVERRIDES: ${{ inputs.plugin-manifest-overrides }}
PLUGINS_MANIFEST_PATH:
${{ github.workspace }}/plugins/plugins.public.yaml
ACTIONS_PATH: ${{ github.action_path }}
run: ${ACTIONS_PATH}/scripts/plugin-overrides.sh

- name: Setup Go for dependency overrides
if: inputs.go-get-overrides != ''
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false
run: |
"${ACTION_PATH}/scripts/plugin-overrides.sh"

- name: Apply go-get dependency overrides
if: inputs.go-get-overrides != ''
- name: Process Go dependency overrides
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
GO_OVERRIDES: ${{ inputs.go-get-overrides }}
run: |
set -e
while IFS= read -r line; do
[ -z "$line" ] && continue
dep="${line%%=*}"
sha="${line#*=}"
[ -z "$dep" ] || [ -z "$sha" ] && continue
echo "Overriding: github.com/smartcontractkit/${dep}@${sha}"
go get "github.com/smartcontractkit/${dep}@${sha}"
done <<< "$GO_OVERRIDES"
go mod tidy
"${ACTION_PATH}/scripts/go-get-overrides.sh"

- name: Free up disk space (to avoid 'no space left on device' errors)
if: inputs.free-disk-space == 'true'
uses: smartcontractkit/.github/actions/free-disk-space@free-disk-space/v1

- name: Resolve cache settings
shell: bash
id: cache-settings
env:
ACTION_PATH: ${{ github.action_path }}
CACHE_MODE: ${{ inputs.cache-mode }}
DOCKER_SAVE_CACHE_INPUT: ${{ inputs.docker-save-cache }}
DOCKER_RESTORE_CACHE_INPUT: ${{ inputs.docker-restore-cache }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
run: |
"${ACTION_PATH}/scripts/resolve-cache-settings.sh" >> "$GITHUB_OUTPUT"

- name: Build push docker image
id: build-image
uses: smartcontractkit/.github/actions/build-push-docker@build-push-docker/v1
Expand All @@ -197,19 +235,17 @@ runs:
docker-attestations: "false"
docker-registry-url: ${{ inputs.docker-registry-url }}
docker-repository-name: ${{ inputs.docker-repository-name }}
# only save on events which are expected to be from the default branch
docker-save-cache:
${{ github.event_name == 'schedule' || github.event_name == 'push' }}
# dont use cache on events which are expected to be from the default branch
# this is to create a fresh cache/snapshot unpolluted by previous cache entries
docker-restore-cache:
${{ github.event_name != 'schedule' && github.event_name != 'push' }}
docker-save-cache: ${{ steps.cache-settings.outputs.save-cache }}
docker-restore-cache: ${{ steps.cache-settings.outputs.restore-cache }}
docker-build-cache-to:
"type=gha,timeout=10m,mode=max,ignore-error=true,compression=zstd,compression-level=3,scope=ctf-build-image-${{
inputs.cache-scope || format('{0}-{1}', runner.os, runner.arch) }}"
docker-build-cache-from:
"type=gha,timeout=10m,scope=ctf-build-image-${{ inputs.cache-scope ||
format('{0}-{1}', runner.os, runner.arch) }}"
cache-mode: ${{ inputs.cache-mode }}
cache-map: ${{ inputs.cache-map || inputs.cache-dance-cache-map }}
cache-dance: ${{ inputs.cache-dance }}

tags: type=raw,value=${{ inputs.image-tag }}
aws-account-number: ${{ inputs.aws-account-number }}
Expand Down
45 changes: 45 additions & 0 deletions actions/ctf-build-image/scripts/go-get-overrides.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/bin/bash
set -e

# Applies Go dependency overrides
# Env inputs: GO_OVERRIDES

if [[ -z "${GO_OVERRIDES}" ]]; then
echo "No Go dependency overrides provided."
exit 0
fi

echo "Applying Go dependency overrides..."
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
if [[ "$line" != *"="* ]]; then
echo "::warning::Skipping malformed override line (missing '='): ${line}"
continue
fi

dep="${line%%=*}"
sha="${line#*=}"
[[ -z "$dep" || -z "$sha" ]] && continue

# Qualify module name if short name provided
if [[ "$dep" != *"/"* ]]; then
module="github.com/smartcontractkit/${dep}"
else
module="${dep}"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use-case for supporting arbitrary overrides, if not I think limiting it to smartcontractkit deps makes the most sense to me.


echo "Replacing Go module: ${module} -> ${sha}"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "[DRY RUN] go mod edit -replace ${module}=${sha}"
else
if [[ "$sha" == *"/"* ]]; then
go mod edit -replace "${module}=${sha}"
else
go mod edit -replace "${module}=${module}@${sha}"
fi
fi
done <<< "$GO_OVERRIDES"

if [[ "${DRY_RUN}" != "true" ]]; then
go mod tidy
fi
Loading
Loading