research: evaluate single-source binary build strategy for CI and Docker

Description

The CI pipeline currently builds the Go binary twice from the same source commit:

  1. go_build — compiled on the CI runner (Debian/glibc, gcc) producing the release assets
  2. docker_build → Dockerfile builder stage — compiled inside Alpine (musl, gcc) producing the image binary

These two binaries are linked against different C libraries (glibc vs musl) and built in different environments, so their checksums will always differ even for identical source. This is a known trade-off, not a bug, but it is worth evaluating whether a single-build approach is preferable.

Options

Option A — Docker owns the build (simplest)

Remove go_build as a CI job. The Dockerfile multi-stage build is the single source of truth. Release binaries are extracted from the built image via docker cp. One binary, one hash, same artifact in both the image and the release assets.

Downside: requires Docker Buildx multi-arch support (#38 (closed)) to produce arm64 binaries without a native arm64 runner.

Option B — CI owns the build, Docker just packages (most reproducible)

Strip the builder stage from the Dockerfile. CI compiles the binary; docker_build copies it into the image via COPY. One binary, consistent hash everywhere.

Downside: docker build no longer works standalone without pre-building the binary first.

Option C — Accept two builds, pin toolchains (pragmatic, status quo)

Keep both builds. Ensure both use the same Go version and -trimpath. Behaviour is identical; hashes differ by design (musl vs glibc). This is acceptable as long as the Docker image and release binaries serve distinct deployment targets.

Acceptance Criteria

  • One of the three options chosen and documented as an ADR in docs/ARCHITECTURE.md
  • Pipeline updated to implement the chosen approach (if not Option C)
  • Release binary and Docker image confirmed to contain the same binary (if Option A or B)

Additional information

Option A becomes the natural choice once #38 (closed) (multi-arch Docker images) is implemented, since Buildx can produce both linux/amd64 and linux/arm64 images in one invocation and binaries can be extracted from the manifest. At that point go_build becomes redundant.