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
36 changes: 36 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ inputs:
Set to true verify the cosign signature for the Blue-Build cli
required: false
default: "false"
skip_if_base_unchanged:
description: |
When set to 'true', the build will be skipped if the base image digest has not changed since the last run.
The base image digest is cached using a key derived from the OS, recipe file hash, and base image reference.
Requires 'yq' and 'skopeo' to be available on the runner.
required: false
default: "false"

runs:
using: "composite"
Expand Down Expand Up @@ -292,6 +299,34 @@ runs:
fi
echo "recipe_path=${RECIPE_PATH}" >> "${GITHUB_OUTPUT}"

- name: Restore base image digest cache
if: ${{ inputs.skip_if_base_unchanged == 'true' }}
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: .base-image-digest
key: ${{ runner.os }}-base-digest-${{ hashFiles(steps.build_vars.outputs.recipe_path) }}-${{ steps.build_vars.outputs.recipe_path }}
restore-keys: |
${{ runner.os }}-base-digest-${{ hashFiles(steps.build_vars.outputs.recipe_path) }}-
${{ runner.os }}-base-digest-

- name: Check base image digest
id: check_base_digest
if: ${{ inputs.skip_if_base_unchanged == 'true' }}
shell: bash
env:
RECIPE_PATH: ${{ steps.build_vars.outputs.recipe_path }}
github_action_path: ${{ github.action_path }}
run: |
set +e
bash "${github_action_path}/check_base_image.sh" "${RECIPE_PATH}"
EXIT_CODE=$?
set -e
if [ "${EXIT_CODE}" -eq 0 ]; then
echo "should_skip_build=true" >> "${GITHUB_OUTPUT}"
else
echo "should_skip_build=false" >> "${GITHUB_OUTPUT}"
fi

- name: Install BlueBuild
shell: bash
env:
Expand All @@ -318,6 +353,7 @@ runs:

# blue-build/cli does the heavy lifting
- name: Build Image
if: ${{ steps.check_base_digest.outputs.should_skip_build != 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
env:
Expand Down
100 changes: 100 additions & 0 deletions check_base_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# check_base_image.sh - Check if the base image digest has changed
#
# Usage: check_base_image.sh <recipe_path>
#
# Exits 0 if the base image digest is unchanged (skip build)
# Exits 1 if the digest has changed, is unavailable, or no cache exists (proceed with build)
#
# Cache file: .base-image-digest (contains the last known digest string)

set -uo pipefail

RECIPE_PATH="${1:-}"
CACHE_FILE=".base-image-digest"

# --- Argument validation ---
if [[ -z "${RECIPE_PATH}" ]]; then
echo "Error: recipe path argument is required." >&2
echo "Usage: check_base_image.sh <recipe_path>" >&2
exit 1
fi

if [[ ! -f "${RECIPE_PATH}" ]]; then
echo "Warning: recipe file not found at '${RECIPE_PATH}'. Proceeding with build." >&2
exit 1
fi

parse_yaml_field() {
local field="${1}"
local file="${2}"
# mikefarah/yq v4 syntax
yq e ".\"${field}\" // \"\"" "${file}" 2>/dev/null
}

# --- Parse recipe fields ---
echo "Checking base image for recipe: ${RECIPE_PATH}"

BASE_IMAGE="$(parse_yaml_field "base-image" "${RECIPE_PATH}")"
IMAGE_VERSION="$(parse_yaml_field "image-version" "${RECIPE_PATH}")"

if [[ -z "${BASE_IMAGE}" ]]; then
echo "Warning: 'base-image' field not found in recipe. Proceeding with build." >&2
exit 1
fi

if [[ -z "${IMAGE_VERSION}" ]]; then
echo "Warning: 'image-version' field not found in recipe. Proceeding with build." >&2
exit 1
fi

IMAGE_REF="${BASE_IMAGE}:${IMAGE_VERSION}"
echo "Base image reference: ${IMAGE_REF}"

# --- Get current digest with 30s timeout ---
# Use skopeo to query the image digest directly from the registry without pulling the image
get_image_digest() {
local image="${1}"
timeout 30s skopeo inspect --format '{{.Digest}}' --retry-times 2 "docker://${image}" 2>/dev/null
}

echo "Fetching current digest for ${IMAGE_REF} ..."
CURRENT_DIGEST=""

if ! CURRENT_DIGEST="$(get_image_digest "${IMAGE_REF}")"; then
echo "Warning: Failed to fetch manifest for '${IMAGE_REF}' (timeout or error). Proceeding with build." >&2
exit 1
fi

if [[ -z "${CURRENT_DIGEST}" ]]; then
echo "Warning: Could not extract digest from manifest for '${IMAGE_REF}'. Proceeding with build." >&2
exit 1
fi

echo "Current digest: ${CURRENT_DIGEST}"

# --- Compare with cached digest ---
if [[ ! -f "${CACHE_FILE}" ]]; then
echo "No cache file found (${CACHE_FILE}). Saving digest and proceeding with build."
printf '%s\n' "${CURRENT_DIGEST}" >"${CACHE_FILE}"
exit 1
fi

CACHED_DIGEST="$(cat "${CACHE_FILE}" 2>/dev/null || true)"

if [[ -z "${CACHED_DIGEST}" ]]; then
echo "Cache file is empty. Saving digest and proceeding with build."
printf '%s\n' "${CURRENT_DIGEST}" >"${CACHE_FILE}"
exit 1
fi

echo "Cached digest: ${CACHED_DIGEST}"

if [[ "${CURRENT_DIGEST}" == "${CACHED_DIGEST}" ]]; then
echo "Base image is unchanged. Skipping build."
exit 0
else
echo "Base image has changed! Updating cache and proceeding with build."
printf '%s\n' "${CURRENT_DIGEST}" >"${CACHE_FILE}"
exit 1
fi