| # 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" |