rgoussu@goussu: ~/library/go/modules-and-build
~/library/go/modules-and-build cat building-and-releasing.md

Building & releasing Go software

# From go build to shipped release — reproducible builds, cross-compilation, GoReleaser automation, container patterns, and supply-chain posture.

Conceptsaved 2026-08-09 #go#build#release#containers#security#ci

Overview

Go's static binaries and built-in cross-compilation make releasing unusually mechanical: one CI job can emit a whole OS/arch matrix with no cross-toolchains (as long as cgo stays off). The craft is everything around the compile — making builds reproducible and verifiable, stamping versions, packing assets, automating archives/changelogs/packages with GoReleaser, choosing a container pattern, and attaching the supply-chain artifacts (SBOM, signatures, provenance) that modern consumers expect.

Key points

  • Reproducible builds are achievable: -trimpath strips build-machine paths, the toolchain is deterministic, and since Go 1.18 binaries carry embedded build info (VCS revision, dirty flag, module versions) readable with go version -m ./binary.
  • Cross-compilation is env vars: GOOS=linux GOARCH=arm64 go build — a release matrix is a nested loop, not a toolchain-installation project. cgo breaks this (CGO_ENABLED=0 is the release-build default for a reason).
  • Version stamping, two ways: classic -ldflags "-X main.version=v1.2.3" injection at link time, vs reading the stamped VCS data via runtime/debug.ReadBuildInfo() — prefer the latter for revision/date, keep ldflags for the human-facing tag CI knows and the build info doesn't.
  • //go:embed compiles assets (templates, migrations, static web files) into the binary — preserving single-file deployment where other stacks grow an assets directory.
  • GoReleaser is the de-facto release automation: from one .goreleaser.yaml it builds the matrix, packs archives, generates changelogs, publishes GitHub releases, Homebrew taps and Scoop manifests, nfpm deb/rpm packages, multi-arch Docker manifests, and hooks signing (cosign) and SBOMs (syft).
  • Container patterns: multi-stage Dockerfile ending FROM scratch or distroless; ko skips Dockerfiles entirely for Kubernetes workloads, building images straight from the importpath and stamping SBOMs.
  • FROM scratch pitfalls: no CA certificates (TLS calls fail), no tzdata (time location lookups fail), no /etc/passwd for a non-root user, no shell for debugging — copy in what you need or use gcr.io/distroless/static, which fixes the first three.
  • Supply-chain posture: SLSA provenance from CI (e.g. the generic SLSA3 GitHub generator or GoReleaser's attestation support), plus a govulncheck gate — it reports only vulnerabilities in code paths you actually call, so it is quiet enough to make blocking.

Details

Reproducibility checklist

  • Build with -trimpath and CGO_ENABLED=0; pin the toolchain (the toolchain directive or a versioned container image).
  • Don't inject timestamps; derive dates from the VCS commit if needed.
  • Verify: build twice on different machines and compare hashes; inspect what shipped with go version -m — it lists module versions, build flags, and VCS state, which is also invaluable for incident response ("which binary has the vulnerable dependency?").

Release pipeline shape (CI)

  1. Tag push v* triggers the release workflow.
  2. Gate: go test ./..., lint, govulncheck ./....
  3. GoReleaser builds the matrix, packs, signs (cosign keyless against the CI OIDC identity), attaches SBOMs (syft), publishes archives + packages + images.
  4. Provenance attestation uploaded alongside the artifacts (SLSA).

Examples

# .goreleaser.yaml — minimal but production-shaped
builds:
  - main: ./cmd/widget
    env: [CGO_ENABLED=0]
    goos: [linux, darwin, windows]
    goarch: [amd64, arm64]
    flags: [-trimpath]
    ldflags:
      - -s -w -X main.version={{ .Version }}
archives:
  - formats: [tar.gz]
checksum:
  name_template: checksums.txt
sboms:
  - artifacts: archive          # syft-generated SBOM per archive
changelog:
  use: github
# Multi-stage: build in the toolchain image, ship almost nothing
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download            # cached layer: deps change rarely
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/widget ./cmd/widget

FROM gcr.io/distroless/static:nonroot   # ca-certs + tzdata + nonroot included
COPY --from=build /out/widget /widget
ENTRYPOINT ["/widget"]

Related

  • Modules & the Go build ecosystem — the parent summary note.
  • Go modules — the dependency side: what go.sum and the checksum database already guarantee before your build starts.
  • TinyGo — the alternative compiler when the target is microcontrollers or small WASM, with its own build constraints.
  • Security testing — where govulncheck and friends sit in the wider verification strategy.
  • Secrets management — signing keys and registry credentials in CI deserve the same discipline as production secrets.