Skip to content

Instantly share code, notes, and snippets.

@Radiergummi
Created November 8, 2025 15:12
Show Gist options
  • Select an option

  • Save Radiergummi/eec012d154ce0400032f3b1ad96451ac to your computer and use it in GitHub Desktop.

Select an option

Save Radiergummi/eec012d154ce0400032f3b1ad96451ac to your computer and use it in GitHub Desktop.
Conditional builds based on context hash
name: Build
on:
workflow_call:
outputs:
image-name:
description: Name of the Docker image built
value: ${{ jobs.finalize.outputs.image-name }}
version:
description: Version of the Docker image built
value: ${{ jobs.finalize.outputs.version }}
jobs:
check-build-cache:
name: Create Image Source Checksum
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
hash: ${{ steps.docker-source-checksum.outputs.hash }}
cache-hit: ${{ steps.set-image-ref.outputs.cache-hit }}
cached-image: ${{ steps.set-image-ref.outputs.image }}
cached-tag: ${{ steps.set-image-ref.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v5
# Calculate a checksum of the Docker build context to use as a cache key. This action makes it possible to
# determine if any of the build inputs have changed since the last build, and thus whether we can reuse a
# previously built image or have to build a new one.
- name: Create Image Source Checksum
id: docker-source-checksum
uses: matchory/docker-source-hash-action@v1.0.1
with:
exclude-paths: |
/.git/
# Now that we have the checksum, we can check the cache for an existing image reference for the same checksum.
# The image reference (name and tag) is stored in a file that we cache using the checksum as part of the cache key.
# This way, if the build inputs haven't changed, we can reuse the previously built image.
- name: Restore Docker Image Reference from Cache
uses: actions/cache/restore@v4
id: docker-cache
with:
key: docker-build-${{ steps.docker-source-checksum.outputs.hash }}
path: .docker_image_ref
# If we found a cached image reference, we need to verify that the image actually exists in the registry.
# It's possible that the image was deleted from the registry but the cache entry still exists, so we need
# to check for that.
# If the image exists, we set the outputs accordingly so that the build job can skip the build step.
- name: Set Image Reference
id: set-image-ref
shell: bash
# language=bash
run: |
# Check for a cached image reference
echo "::group::Checking for cached Docker image"
# Read the image reference from the cached file, ignoring errors if the file doesn't exist
image_ref=$(cat .docker_image_ref 2>/dev/null || true)
image=""
tag=""
# Check if image with that tag exists in the registry
if [ -n "${image_ref}" ] && [ "${{ inputs.skip-cache-check }}" != 'true' ]; then
echo "Found cached image reference: ${image_ref}"
manifest=$(docker manifest inspect "${image_ref}" 2>&1 || true)
status=$?
{
echo "image-manifest<<EOF"
echo "${manifest}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ $status -eq 0 ]; then
echo "Cached image exists in registry."
echo "::debug::Docker manifest inspect output: ${manifest}"
# Split image_ref into image and tag on ':'. See here for how the character soup below works:
# https://stackoverflow.com/a/15988793/2532203
image="${image_ref%%:*}"
tag="${image_ref##*:}"
else
echo "::warning::A cached image reference was found, but the image does not exist in the registry. It may have been deleted and will be rebuilt."
echo "Docker manifest inspect output: ${manifest}"
fi
else
echo "::debug::No cached image reference found or cache check skipped."
fi
echo "::endgroup::"
echo "image=${image}" >> $GITHUB_OUTPUT
echo "tag=${tag}" >> $GITHUB_OUTPUT
if [ -n "${image}" ] && [ -n "${tag}" ]; then
echo "Using cached image: ${image}:${tag}"
echo "cache-hit=true" >> $GITHUB_OUTPUT
else
echo "No cached image found."
echo "cache-hit=false" >> $GITHUB_OUTPUT
fi
# Build the Docker image only if we didn't find a cached image in the previous step.
build:
if: needs.check-build-cache.outputs.cache-hit != 'true'
name: Build Docker Image
needs: check-build-cache
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-name: ghcr.io/${{ github.repository }}
version: ${{ steps.meta.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Docker Metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build and Push
id: build
uses: docker/build-push-action@v6
with:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-${{ steps.meta.outputs.version }}
cache-to: type=registry,mode=max,ref=ghcr.io/${{ github.repository }}:buildcache-${{ steps.meta.outputs.version }}
provenance: mode=max
sbom: true
# Store the built image reference (name and tag) in a file we can store in the cache in the next step.
- name: Store Docker Image Reference
shell: bash
# language=bash
run: echo "ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}" > .docker_image_ref
# Finally, save the image reference to cache so that it can be reused in future runs if the build inputs
# haven't changed.
- name: Save Docker Image Reference to Cache
uses: actions/cache/save@v4
with:
key: docker-build-${{ needs.check-build-cache.outputs.hash }}
path: .docker_image_ref
# Finalize job to output the image name and version, whether from cache or from the build step. By setting this step
# to always run, we ensure that the outputs are always available to downstream jobs, regardless of whether the build
# was skipped and the image info came from cache or was built anew.
finalize:
name: Finalize Build
if: always() && !cancelled()
needs:
- check-build-cache
- build
runs-on: ubuntu-latest
outputs:
image-name: ${{ steps.final.outputs.image-name }}
version: ${{ steps.final.outputs.version }}
steps:
- name: Set outputs
id: final
shell: bash
# language=bash
run: |
echo "image-name=${{ needs.build.outputs.image-name || needs.check-build-cache.outputs.cached-image }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.build.outputs.version || needs.check-build-cache.outputs.cached-tag }}" >> $GITHUB_OUTPUT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment