From ee289c2a435fda6c0d6bc47b246f2cc7e8f37cef Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 11:23:46 +0800 Subject: [PATCH 1/2] Check that each image carries a binary built for its platform The release workflow builds linux/amd64 and linux/arm64, but under QEMU an arm64 build succeeds even when the Dockerfile compiles for amd64, and the published arm64 image then cannot start. On pull requests, build every image for both platforms and compare the ELF machine of the binary inside with the platform. Nothing is pushed. --- .github/workflows/image-check.yml | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/image-check.yml diff --git a/.github/workflows/image-check.yml b/.github/workflows/image-check.yml new file mode 100644 index 0000000..26f184e --- /dev/null +++ b/.github/workflows/image-check.yml @@ -0,0 +1,72 @@ +name: image-check + +# Build every image for each published platform and check that the binary inside +# was compiled for that platform. A hardcoded GOARCH once shipped an amd64 binary +# in the arm64 image: the emulated arm64 build succeeds, so only looking at the +# binary catches it. Runs on pull requests; nothing is pushed. +on: + pull_request: + paths: + - 'Dockerfile*' + - '**.go' + - 'go.mod' + - 'go.sum' + - '.github/workflows/image-check.yml' + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: [linux/amd64, linux/arm64] + include: + - images: >- + Dockerfile=/usr/local/bin/autoconfig + Dockerfile.reload=/usr/local/bin/reload + Dockerfile.hagate=/usr/local/bin/hagate + steps: + - name: Check out + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + + - name: Set up Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Build and check each image + env: + PLATFORM: ${{ matrix.platform }} + IMAGES: ${{ matrix.images }} + run: | + set -eu + arch=${PLATFORM#linux/} + case "$arch" in + amd64) want="X86-64" ;; + arm64) want="AArch64" ;; + *) echo "no ELF machine known for $arch" >&2; exit 1 ;; + esac + for spec in $IMAGES; do + file=${spec%%=*}; bin=${spec#*=} + echo "::group::$file ($PLATFORM) -> $bin" + docker buildx build --platform "$PLATFORM" --load -f "$file" -t "check:$arch" . + got=$(docker image inspect --format '{{.Architecture}}' "check:$arch") + [ "$got" = "$arch" ] || { echo "::error::$file: image architecture $got, want $arch"; exit 1; } + id=$(docker create --platform "$PLATFORM" "check:$arch") + docker cp "$id:$bin" ./bin.check + docker rm "$id" >/dev/null + machine=$(readelf -h ./bin.check | sed -n 's/^ *Machine: *//p') + echo "$file $bin: $machine" + case "$machine" in + *"$want"*) ;; + *) echo "::error::$file: $bin is built for '$machine', the $PLATFORM image needs $want"; exit 1 ;; + esac + rm -f ./bin.check + echo "::endgroup::" + done From a93614e9f104ab70c676a436b95abe20277bec2e Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 11:37:55 +0800 Subject: [PATCH 2/2] Build the arm64 images with arm64 binaries The three Dockerfiles hardcoded GOARCH=amd64, so the linux/arm64 images of autoconfig, autoconfig-reload and autoconfig-hagate published by the multi-platform release build contained amd64 binaries and could not start on arm64 nodes. Take GOARCH from TARGETARCH, which buildx sets to the platform being built; a plain docker build leaves it empty and Go builds for the host as before. --- Dockerfile | 7 ++++++- Dockerfile.hagate | 7 ++++++- Dockerfile.reload | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a12861f..8904ea2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,12 @@ ARG GOPROXY=https://proxy.golang.org,direct FROM ${GO_BASE} AS build ARG GOPROXY -ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=amd64 +# TARGETARCH is set by buildx to the platform being built, so a multi-platform build +# compiles each image for its own arch. It was hardcoded to amd64, which put an amd64 +# binary into the published arm64 image. A plain `docker build` leaves it empty and Go +# then builds for the host, as before. +ARG TARGETARCH +ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} WORKDIR /src COPY go.mod go.sum ./ RUN go mod download diff --git a/Dockerfile.hagate b/Dockerfile.hagate index 22d0ba5..e44b1fe 100644 --- a/Dockerfile.hagate +++ b/Dockerfile.hagate @@ -10,7 +10,12 @@ ARG GOPROXY=https://proxy.golang.org,direct FROM ${GO_BASE} AS build ARG GOPROXY -ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=amd64 +# TARGETARCH is set by buildx to the platform being built, so a multi-platform build +# compiles each image for its own arch. It was hardcoded to amd64, which put an amd64 +# binary into the published arm64 image. A plain `docker build` leaves it empty and Go +# then builds for the host, as before. +ARG TARGETARCH +ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} WORKDIR /src COPY go.mod go.sum ./ RUN go mod download diff --git a/Dockerfile.reload b/Dockerfile.reload index 99bcffe..f0806d6 100644 --- a/Dockerfile.reload +++ b/Dockerfile.reload @@ -9,7 +9,12 @@ ARG GOPROXY=https://proxy.golang.org,direct FROM ${GO_BASE} AS build ARG GOPROXY -ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=amd64 +# TARGETARCH is set by buildx to the platform being built, so a multi-platform build +# compiles each image for its own arch. It was hardcoded to amd64, which put an amd64 +# binary into the published arm64 image. A plain `docker build` leaves it empty and Go +# then builds for the host, as before. +ARG TARGETARCH +ENV GOPROXY=${GOPROXY} GOSUMDB=off GOTOOLCHAIN=local CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} WORKDIR /src COPY go.mod go.sum ./ RUN go mod download