blob: a140117e528f91e3a54a1ffb4b12ff259361790b [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: docker-buildx
description: Multi-arch Docker build and push
inputs:
task:
description: "Task to run (build/publish/docker)"
required: false
default: "build"
component:
description: "Component key from .github/config/publish.yml (e.g. rust-server)"
required: false
default: ""
version:
description: "Version tag (use 'edge' for master post-merge)"
required: false
default: "test"
context:
description: "Build context"
required: false
default: "."
dry_run:
description: "Dry run mode"
required: false
default: "false"
libc:
description: "Libc to use (glibc/musl)"
required: false
default: "musl"
platform:
description: "Single platform to build (e.g., linux/amd64). If set, builds only this platform without QEMU. Leave empty for multi-arch build."
required: false
default: ""
gha-cache:
description: "Whether to use GitHub Actions cache for Docker layers (disable to save cache space)"
required: false
default: "true"
outputs:
digest:
description: "Image digest (sha256:...)"
value: ${{ steps.build.outputs.digest }}
image:
description: "Registry image name"
value: ${{ steps.config.outputs.image }}
runs:
using: "composite"
steps:
- name: Resolve component from publish.yml
id: config
shell: bash
run: |
set -euxo pipefail
comp="${{ inputs.component }}"
image=""
dockerfile=""
cfg_platforms=""
if [ -n "$comp" ]; then
if ! command -v yq >/dev/null 2>&1; then
YQ_VERSION="v4.47.1"
YQ_CHECKSUM="0fb28c6680193c41b364193d0c0fc4a03177aecde51cfc04d506b1517158c2fb"
wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64
echo "${YQ_CHECKSUM} /tmp/yq" | sha256sum -c - || exit 1
chmod +x /tmp/yq
sudo mv /tmp/yq /usr/local/bin/yq
fi
components_b64="$(yq -o=json -I=0 '.components' .github/config/publish.yml | base64 -w0)"
JSON_B64="$components_b64" COMP="$comp" node -e '
const b64 = process.env.JSON_B64 || "";
const comp = process.env.COMP || "";
if (!b64) { console.error("Missing publish.yml components"); process.exit(1); }
const cfg = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
const e = cfg[comp];
if (!e) { console.error(`Component not found in publish.yml: ${comp || "<empty>"}`); process.exit(1); }
const out = {
image: e.image || "",
dockerfile: e.dockerfile || "Dockerfile",
platforms: Array.isArray(e.platforms) ? e.platforms.join(",") : (e.platforms || "")
};
process.stdout.write(JSON.stringify(out));
' > /tmp/_dockercfg.json
image="$(jq -r .image /tmp/_dockercfg.json)"
dockerfile="$(jq -r .dockerfile /tmp/_dockercfg.json)"
cfg_platforms="$(jq -r .platforms /tmp/_dockercfg.json)"
fi
should_push="false"
if [ "${{ inputs.task }}" = "publish" ] && [ "${{ inputs.dry_run }}" != "true" ]; then
should_push="true"
fi
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "dockerfile=$dockerfile" >> "$GITHUB_OUTPUT"
echo "cfg_platforms=$cfg_platforms" >> "$GITHUB_OUTPUT"
echo "should_push=$should_push" >> "$GITHUB_OUTPUT"
echo "🐳 Config:"
echo " component: ${comp:-<none>}"
echo " image: ${image:-<unset>}"
echo " dockerfile:${dockerfile:-<unset>}"
echo " push: ${should_push}"
echo " version: ${{ inputs.version }}"
if [ -n "$comp" ] && [ -z "$image" ]; then
echo "Component '${comp}' missing image mapping in publish.yml" >&2
exit 1
fi
- name: Set up QEMU
# Skip QEMU when building single platform on native runner (no emulation needed)
if: inputs.platform == ''
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
image=moby/buildkit:latest
- name: Login to Docker Hub
if: steps.config.outputs.should_push == 'true'
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Compute platform suffix
id: suffix
shell: bash
run: |
platform="${{ inputs.platform }}"
if [ -n "$platform" ]; then
# Extract arch from platform (e.g., linux/amd64 -> amd64)
suffix="${platform##*/}"
echo "suffix=-${suffix}" >> "$GITHUB_OUTPUT"
echo "is_single=true" >> "$GITHUB_OUTPUT"
echo "📦 Platform suffix: -${suffix}"
else
echo "suffix=" >> "$GITHUB_OUTPUT"
echo "is_single=false" >> "$GITHUB_OUTPUT"
echo "📦 No platform suffix (multi-arch)"
fi
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.config.outputs.image }}
# Tags are only used for local builds (dry-run). Push mode always uses digest.
tags: |
type=raw,value=${{ inputs.version }}${{ steps.suffix.outputs.suffix }},enable=${{ steps.config.outputs.should_push != 'true' }}
type=sha,enable=${{ steps.config.outputs.should_push != 'true' && inputs.version == 'test' }}
- name: Determine platforms
id: platforms
shell: bash
run: |
p_input="${{ inputs.platform }}"
p_cfg="${{ steps.config.outputs.cfg_platforms }}"
# Priority: input platform > config platforms > defaults
if [ -n "$p_input" ]; then
# Single platform specified - native build on correct runner
platforms="$p_input"
echo "🚀 Single platform build (native): $platforms"
elif [ -n "$p_cfg" ]; then
platforms="$p_cfg"
echo "🖥️ Platforms from config: $platforms"
elif [ "${{ steps.config.outputs.should_push }}" = "true" ]; then
platforms="linux/amd64,linux/arm64"
echo "🖥️ Multi-arch build: $platforms"
else
platforms="linux/amd64"
echo "🖥️ Default platform: $platforms"
fi
echo "platforms=$platforms" >> "$GITHUB_OUTPUT"
- name: Compose cache config (no registry on dry-run)
id: cachecfg
shell: bash
run: |
set -euo pipefail
comp="${{ inputs.component }}"
os="${{ runner.os }}"
arch="${{ runner.arch }}"
img="${{ steps.config.outputs.image }}"
# Include architecture in cache scope to prevent collisions between
# native amd64 and arm64 builds (both have runner.os = "Linux")
scope_arch="${comp}-${os}-${arch}"
scope_os="${comp}-${os}"
# Shared deps cache image - apache/iggy has the most deps, others can reuse
shared_deps="apache/iggy"
if [ "${{ inputs.dry_run }}" = "true" ]; then
# dry-run/forks: avoid Docker Hub completely, use shared GHA scope for deps
CACHE_TO=$(
printf '%s\n' \
"type=gha,scope=buildkit-deps-${os}-${arch},mode=max" \
"type=gha,scope=buildkit-${scope_arch},mode=max" \
"type=inline"
)
CACHE_FROM=$(
printf '%s\n' \
"type=gha,scope=buildkit-deps-${os}-${arch}" \
"type=gha,scope=buildkit-${scope_arch}" \
"type=gha,scope=buildkit-${scope_os}"
)
else
# CI on upstream: use shared deps cache + component-specific cache
# Order matters: try shared deps first (most likely to have common layers)
# Avoid duplicates: if img == shared_deps, skip the component-specific registry cache
if [ "$img" = "$shared_deps" ]; then
CACHE_TO=$(
printf '%s\n' \
"type=registry,ref=${shared_deps}:buildcache-${arch},mode=max" \
"type=inline" \
"type=gha,scope=buildkit-deps-${os}-${arch},mode=max" \
"type=gha,scope=buildkit-${scope_arch},mode=max"
)
CACHE_FROM=$(
printf '%s\n' \
"type=registry,ref=${shared_deps}:buildcache-${arch}" \
"type=registry,ref=${shared_deps}:buildcache" \
"type=registry,ref=${img}:latest" \
"type=gha,scope=buildkit-deps-${os}-${arch}" \
"type=gha,scope=buildkit-${scope_arch}" \
"type=gha,scope=buildkit-${scope_os}"
)
else
CACHE_TO=$(
printf '%s\n' \
"type=registry,ref=${shared_deps}:buildcache-${arch},mode=max" \
"type=registry,ref=${img}:buildcache-${arch},mode=max" \
"type=inline" \
"type=gha,scope=buildkit-deps-${os}-${arch},mode=max" \
"type=gha,scope=buildkit-${scope_arch},mode=max"
)
CACHE_FROM=$(
printf '%s\n' \
"type=registry,ref=${shared_deps}:buildcache-${arch}" \
"type=registry,ref=${shared_deps}:buildcache" \
"type=registry,ref=${img}:buildcache-${arch}" \
"type=registry,ref=${img}:buildcache" \
"type=registry,ref=${img}:latest" \
"type=gha,scope=buildkit-deps-${os}-${arch}" \
"type=gha,scope=buildkit-${scope_arch}" \
"type=gha,scope=buildkit-${scope_os}"
)
fi
fi
# Filter out GHA cache entries if gha-cache is disabled
if [ "${{ inputs.gha-cache }}" = "false" ]; then
CACHE_TO=$(echo "$CACHE_TO" | grep -v "type=gha" || true)
CACHE_FROM=$(echo "$CACHE_FROM" | grep -v "type=gha" || true)
fi
{
echo 'CACHE_TO<<EOF'
echo "$CACHE_TO"
echo 'EOF'
echo 'CACHE_FROM<<EOF'
echo "$CACHE_FROM"
echo 'EOF'
} >> "$GITHUB_ENV"
echo "Computed cache-to:"
printf '%s\n' "$CACHE_TO"
echo "Computed cache-from:"
printf '%s\n' "$CACHE_FROM"
- name: Determine Rust toolchain version (labels)
shell: bash
run: |
if [[ -f rust-toolchain.toml ]]; then
ver="$(sed -En 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' rust-toolchain.toml | head -1)"
: "${ver:=unknown}"
echo "RUST_VERSION=$ver" >> "$GITHUB_ENV"
echo "Using toolchain: $ver"
else
echo "RUST_VERSION=1.92" >> "$GITHUB_ENV"
echo "No rust-toolchain.toml found; labeling as 1.92"
fi
- name: Compose build args
id: bargs
shell: bash
run: |
set -euo pipefail
args="VERSION=${{ inputs.version }}
BUILD_DATE=${{ github.event.repository.updated_at }}
VCS_REF=${{ github.sha }}
RUST_VERSION=${{ env.RUST_VERSION }}
LIBC=${{ inputs.libc }}
IGGY_CI_BUILD=true"
{
echo "all<<__EOF__"
echo "$args"
echo "__EOF__"
} >> "$GITHUB_OUTPUT"
echo "Build args:"
printf '%s\n' "$args"
- name: Determine build context
id: ctx
shell: bash
run: |
# For web-ui, the context should be the web directory
if [ "${{ inputs.component }}" = "web-ui" ]; then
context="web"
else
context="${{ inputs.context }}"
fi
echo "context=$context" >> "$GITHUB_OUTPUT"
echo "📁 Build context: $context"
- name: Build and push (by digest)
id: build-push
if: steps.config.outputs.should_push == 'true'
uses: docker/build-push-action@v6
with:
context: ${{ steps.ctx.outputs.context }}
file: ${{ steps.config.outputs.dockerfile }}
platforms: ${{ steps.platforms.outputs.platforms }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: ${{ env.CACHE_FROM }}
cache-to: ${{ env.CACHE_TO }}
build-args: |
${{ steps.bargs.outputs.all }}
outputs: type=image,name=${{ steps.config.outputs.image }},push-by-digest=true,name-canonical=true,push=true
- name: Build only (dry-run)
id: build-only
if: steps.config.outputs.should_push != 'true'
uses: docker/build-push-action@v6
with:
context: ${{ steps.ctx.outputs.context }}
file: ${{ steps.config.outputs.dockerfile }}
platforms: ${{ steps.platforms.outputs.platforms }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: ${{ env.CACHE_FROM }}
cache-to: ${{ env.CACHE_TO }}
build-args: |
${{ steps.bargs.outputs.all }}
push: false
- name: Consolidate build output
id: build
shell: bash
run: |
# Output digest from push step (dry-run builds don't produce useful digests)
echo "digest=${{ steps.build-push.outputs.digest }}" >> "$GITHUB_OUTPUT"
- name: Export image (if not pushing)
if: steps.config.outputs.should_push == 'false' && inputs.task != 'publish'
shell: bash
run: |
echo "Built: ${{ steps.config.outputs.image }}:${{ inputs.version }}"
echo "To save: docker save ${{ steps.config.outputs.image }}:${{ inputs.version }} -o image.tar"
- name: Summary
if: always()
shell: bash
run: |
{
echo "## 🐳 Docker Build Summary"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| Image | \`${{ steps.config.outputs.image }}\` |"
echo "| Version | \`${{ inputs.version }}\` |"
echo "| Dockerfile | \`${{ steps.config.outputs.dockerfile }}\` |"
echo "| Platforms | \`${{ steps.platforms.outputs.platforms }}\` |"
echo "| Pushed | ${{ steps.config.outputs.should_push }} |"
if [ -n "${{ steps.build.outputs.digest }}" ]; then
echo "| Digest | \`${{ steps.build.outputs.digest }}\` |"
fi
echo ""
if [ "${{ steps.config.outputs.should_push }}" = "true" ]; then
echo "### Digest (for manifest creation)"
echo '```'
echo "${{ steps.config.outputs.image }}@${{ steps.build.outputs.digest }}"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"