name: go-release description: Release engineering for Go projects — GoReleaser v2 with version ldflags into an internal/version package, SBOMs and multi-arch container images, tag-triggered GitHub Actions releases, CI running build/vet/test -race/govulncheck with SHA-pinned actions, and Dependabot coverage for gomod, tools, npm, and Actions. Use when setting up releases or GoReleaser for a Go project, writing GitHub Actions CI or release workflows for a Go repository, stamping version metadata into a Go binary at build time, or adding Dependabot coverage to a Go repo. license: MIT
Go release engineering
Tag-driven releases: pushing a vX.Y.Z tag builds cross-platform archives, checksums, SBOMs, and
multi-arch container images via GoReleaser. CI gates every push; Dependabot keeps everything —
including the action pins — fresh. This layers on the layout and Makefile from the go-project
skill.
1. Stamp version metadata into internal/version
// Package version exposes build metadata stamped into the binary at link time.
// Keep these as vars (not consts) so -ldflags "-X ..." can rewrite them; an
// unbuilt or `go run` binary reports the defaults.
package version
var (
Version = "dev" // semver release tag, e.g. v1.2.3
Commit = "none" // short git SHA
BuildDate = "unknown" // RFC3339 UTC build timestamp
)
Both make build and GoReleaser inject the same vars at the same import path:
-ldflags "-s -w -X <module>/internal/version.Version={{ .Version }} ..."
Expose the metadata through --version output or a GET /version endpoint.
2. GoReleaser config
Copy templates/goreleaser.yaml to .goreleaser.yaml and replace the
myapp / OWNER/REPO placeholders. What it encodes:
- Static, reproducible binaries:
CGO_ENABLED=0,-trimpath,-s -w, for linux + darwin × amd64 + arm64. tar.gzarchives shipping theLICENSE, achecksums.txt, and an SBOM per archive — generated by thesyftpinned intools/go.modand invoked viago tool, so nothing needs to be onPATH.- A changelog that excludes
docs:/test:/chore:/ci:conventional-commit prefixes. dockers_v2multi-arch images (buildx) pushed to GHCR with OCI annotations.
Validate locally with make snapshot (goreleaser release --snapshot --clean) — full build, no
publish.
3. CI workflow
Copy templates/ci.yaml to .github/workflows/ci.yaml. Every push and pull
request runs go build, go vet, go test -race, and govulncheck. Conventions:
- The Go version comes from
go.mod(go-version-file), never hard-coded in the workflow. - Every action is pinned to a full commit SHA with the tag in a trailing comment — a moved tag can never change what runs. Dependabot keeps the pins fresh.
permissions: contents: read— the default token does nothing else.
4. Release workflow
Copy templates/release.yaml to .github/workflows/release.yaml. It
triggers on v* tags, checks out with fetch-depth: 0 (GoReleaser needs full history for the
changelog), sets up QEMU + buildx for multi-arch images, logs into GHCR with the built-in
GITHUB_TOKEN, and runs goreleaser release --clean — SBOMs come from the pinned syft via
go tool, so there is no separate syft install step. Cutting a release is exactly: push a semver
tag.
5. Dependabot
Copy templates/dependabot.yaml to .github/dependabot.yaml: daily
checks with a 7-day cooldown, minor + patch bumps grouped into one PR per ecosystem (majors arrive
as individual PRs). tools/go.mod gets its own gomod entry — Dependabot does not descend into
nested modules — so developer-tool bumps never ride along with application dependency PRs, and the
github-actions entry keeps the workflow SHA pins fresh. For the rationale behind each knob and
the full ecosystem matrix (docker, docker-compose, uv, …), see reference.md.