From f6c9e769d707758dbc163859bb3ff91d9d2e1f99 Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 11:23:50 +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 | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 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..1107212 --- /dev/null +++ b/.github/workflows/image-check.yml @@ -0,0 +1,70 @@ +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/hang-watcher + 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 ab7255bc11e39465c83623940adf1d3387bf3a3f Mon Sep 17 00:00:00 2001 From: zhanghao Date: Wed, 28 Oct 2026 11:28:20 +0800 Subject: [PATCH 2/2] Build the arm64 image with an arm64 binary The Dockerfile hardcoded GOARCH=amd64, so the linux/arm64 image published by the multi-platform release build contained an amd64 binary 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 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4104cdb..f2f5e37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,12 @@ ARG GO_IMAGE=docker.io/library/golang:1.24-bookworm ARG RUNTIME_IMAGE=docker.io/library/debian:12-slim FROM ${GO_IMAGE} AS build -ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOSUMDB=off GOTOOLCHAIN=local +# 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 CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GOSUMDB=off GOTOOLCHAIN=local WORKDIR /src COPY go.mod ./ COPY *.go ./