From cbb6163cc2f24d32480039f05a7a8f76bba92d71 Mon Sep 17 00:00:00 2001 From: estebanthi Date: Sun, 4 Jan 2026 18:42:08 +0100 Subject: [PATCH] Optimize docker build workflow --- .github/workflows/docker-build-publish.yml | 209 +++++++++++++++------ 1 file changed, 156 insertions(+), 53 deletions(-) diff --git a/.github/workflows/docker-build-publish.yml b/.github/workflows/docker-build-publish.yml index 3ab9777..44d5fc5 100644 --- a/.github/workflows/docker-build-publish.yml +++ b/.github/workflows/docker-build-publish.yml @@ -101,63 +101,166 @@ jobs: run: | set -euo pipefail - SSH_FLAGS="" + SSH_FLAGS=() if [ -n "${SSH_AUTH_SOCK:-}" ]; then - SSH_FLAGS="--ssh default" + SSH_FLAGS+=(--ssh default) fi - echo "$IMAGES" | jq -c '.[]' | while read -r img; do - IMAGE_NAME=$(echo "$img" | jq -r '.name') - FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}" - CACHE_REF="${{ inputs.registry_host }}/$(echo "$img" | jq -r '.cache_ref')" - CONTEXT=$(echo "$img" | jq -r '.context') - DOCKERFILE=$(echo "$img" | jq -r '.dockerfile') - TARGET=$(echo "$img" | jq -r '.target') - RAW_REF="${{ github.ref }}" - SHA_FULL="${{ github.sha }}" - SHA_SHORT="${SHA_FULL:0:12}" + RAW_REF="${{ github.ref }}" + SHA_FULL="${{ github.sha }}" + SHA_SHORT="${SHA_FULL:0:12}" - echo "==== Building $FULL_IMAGE ====" + VERSION_TAGS=() + if [[ "$RAW_REF" =~ ^refs/tags/v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + VERSION_TAGS+=("v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}") + VERSION_TAGS+=("v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}") + VERSION_TAGS+=("v${BASH_REMATCH[1]}") + VERSION_TAGS+=("latest") + fi - TAGS=() - TAGS+=("$FULL_IMAGE:sha-$SHA_SHORT") - if [[ "$RAW_REF" =~ ^refs/tags/v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - TAGS+=("$FULL_IMAGE:v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}") - TAGS+=("$FULL_IMAGE:v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}") - TAGS+=("$FULL_IMAGE:v${BASH_REMATCH[1]}") - TAGS+=("$FULL_IMAGE:latest") - fi + BUILD_ARG_FLAGS=() + BUILD_ARGS_JSON="{}" + if [ -n "$BUILD_ARGS" ]; then + for arg in $BUILD_ARGS; do + if [[ "$arg" != *=* ]]; then + echo "Invalid build arg: $arg" >&2 + exit 1 + fi + BUILD_ARG_FLAGS+=(--build-arg "$arg") - TAG_ARGS=$(printf -- "--tag %s " "${TAGS[@]}") - BUILD_ARG_FLAGS="" - if [ -n "$BUILD_ARGS" ]; then - BUILD_ARG_FLAGS=$(printf -- "--build-arg %s " $BUILD_ARGS) - fi - - TARGET_FLAG="" - if [ -n "$TARGET" ] && [ "$TARGET" != "null" ]; then - TARGET_FLAG="--target $TARGET" - fi - - docker buildx build \ - --file "$DOCKERFILE" \ - $TARGET_FLAG \ - --cache-from "type=registry,ref=$CACHE_REF" \ - --cache-to "type=registry,ref=$CACHE_REF,mode=max" \ - $SSH_FLAGS \ - --load \ - $TAG_ARGS \ - $BUILD_ARG_FLAGS \ - "$CONTEXT" - - echo "==== Trivy scan for $FULL_IMAGE ====" - trivy image \ - --severity "$TRIVY_SEVERITY" \ - --exit-code 1 \ - "${TAGS[0]}" - - echo "==== Pushing $FULL_IMAGE ====" - for tag in "${TAGS[@]}"; do - docker push "$tag" + key="${arg%%=*}" + val="${arg#*=}" + BUILD_ARGS_JSON=$(jq --arg k "$key" --arg v "$val" '. + {($k): $v}' <<<"$BUILD_ARGS_JSON") done - done + fi + + while read -r group; do + GROUP_COUNT=$(echo "$group" | jq 'length') + CONTEXT=$(echo "$group" | jq -r '.[0].context') + DOCKERFILE=$(echo "$group" | jq -r '.[0].dockerfile') + + if [ "$GROUP_COUNT" -gt 1 ]; then + echo "==== Building $GROUP_COUNT images from $DOCKERFILE (bake) ====" + + CACHE_REF_SUFFIXES=$(echo "$group" | jq -c 'map(.cache_ref // empty) | map(select(length > 0)) | unique') + CACHE_REF_SUFFIX=$(echo "$CACHE_REF_SUFFIXES" | jq -r '.[0] // empty') + UNIQUE_CACHE_COUNT=$(echo "$CACHE_REF_SUFFIXES" | jq -r 'length') + if [ "$UNIQUE_CACHE_COUNT" -gt 1 ]; then + echo "Warning: multiple cache_ref values for the same dockerfile. Using $CACHE_REF_SUFFIX." >&2 + fi + + CACHE_REF="" + if [ -n "$CACHE_REF_SUFFIX" ]; then + CACHE_REF="${{ inputs.registry_host }}/$CACHE_REF_SUFFIX" + fi + + TARGETS_JSON="{}" + TARGET_NAMES=() + + while read -r entry; do + IDX=$(echo "$entry" | jq -r '.key') + IMG=$(echo "$entry" | jq -c '.value') + + IMAGE_NAME=$(echo "$IMG" | jq -r '.name') + TARGET=$(echo "$IMG" | jq -r '.target // empty') + FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}" + + TAGS=() + TAGS+=("$FULL_IMAGE:sha-$SHA_SHORT") + for ver in "${VERSION_TAGS[@]}"; do + TAGS+=("$FULL_IMAGE:$ver") + done + TAGS_JSON=$(printf '%s\n' "${TAGS[@]}" | jq -R . | jq -s .) + + TARGET_NAME="img_${IDX}" + TARGET_NAMES+=("$TARGET_NAME") + + TARGET_OBJ=$(jq -n \ + --arg context "$CONTEXT" \ + --arg dockerfile "$DOCKERFILE" \ + --arg target "$TARGET" \ + --argjson tags "$TAGS_JSON" \ + --argjson args "$BUILD_ARGS_JSON" \ + --arg cache_ref "$CACHE_REF" \ + '{ + context: $context, + dockerfile: $dockerfile, + tags: $tags, + args: $args + } + + ( ($target != "" and $target != "null") ? {target: $target} : {} ) + + ( ($cache_ref != "") ? {"cache-from": ["type=registry,ref=" + $cache_ref], "cache-to": ["type=registry,ref=" + $cache_ref + ",mode=max"]} : {} )') + + TARGETS_JSON=$(jq -n --arg name "$TARGET_NAME" --argjson target "$TARGET_OBJ" --argjson targets "$TARGETS_JSON" '$targets + {($name): $target}') + done < <(echo "$group" | jq -c 'to_entries[]') + + GROUP_TARGETS_JSON=$(printf '%s\n' "${TARGET_NAMES[@]}" | jq -R . | jq -s .) + BAKE_JSON=$(jq -n --argjson targets "$TARGETS_JSON" --argjson group_targets "$GROUP_TARGETS_JSON" '{target:$targets, group:{default:{targets:$group_targets}}}') + BAKE_FILE=$(mktemp) + echo "$BAKE_JSON" > "$BAKE_FILE" + + docker buildx bake --file "$BAKE_FILE" --push "${SSH_FLAGS[@]}" + rm -f "$BAKE_FILE" + + while read -r img; do + IMAGE_NAME=$(echo "$img" | jq -r '.name') + FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}" + + echo "==== Trivy scan for $FULL_IMAGE ====" + trivy image \ + --severity "$TRIVY_SEVERITY" \ + --exit-code 1 \ + "$FULL_IMAGE:sha-$SHA_SHORT" + done < <(echo "$group" | jq -c '.[]') + else + img=$(echo "$group" | jq -c '.[0]') + + IMAGE_NAME=$(echo "$img" | jq -r '.name') + FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}" + CACHE_REF_SUFFIX=$(echo "$img" | jq -r '.cache_ref // empty') + CONTEXT=$(echo "$img" | jq -r '.context') + DOCKERFILE=$(echo "$img" | jq -r '.dockerfile') + TARGET=$(echo "$img" | jq -r '.target // empty') + + TAGS=() + TAGS+=("$FULL_IMAGE:sha-$SHA_SHORT") + for ver in "${VERSION_TAGS[@]}"; do + TAGS+=("$FULL_IMAGE:$ver") + done + + TAG_ARGS=() + for tag in "${TAGS[@]}"; do + TAG_ARGS+=(--tag "$tag") + done + + TARGET_FLAGS=() + if [ -n "$TARGET" ] && [ "$TARGET" != "null" ]; then + TARGET_FLAGS+=(--target "$TARGET") + fi + + CACHE_FLAGS=() + if [ -n "$CACHE_REF_SUFFIX" ]; then + CACHE_REF="${{ inputs.registry_host }}/${CACHE_REF_SUFFIX}" + CACHE_FLAGS+=(--cache-from "type=registry,ref=$CACHE_REF") + CACHE_FLAGS+=(--cache-to "type=registry,ref=$CACHE_REF,mode=max") + fi + + echo "==== Building $FULL_IMAGE ====" + + docker buildx build \ + --file "$DOCKERFILE" \ + "${TARGET_FLAGS[@]}" \ + "${CACHE_FLAGS[@]}" \ + "${SSH_FLAGS[@]}" \ + --push \ + "${TAG_ARGS[@]}" \ + "${BUILD_ARG_FLAGS[@]}" \ + "$CONTEXT" + + echo "==== Trivy scan for $FULL_IMAGE ====" + trivy image \ + --severity "$TRIVY_SEVERITY" \ + --exit-code 1 \ + "${TAGS[0]}" + fi + done < <(echo "$IMAGES" | jq -c 'sort_by(.context, .dockerfile) | group_by([.context, .dockerfile])[]')