Shipping: Version, Install, Completion, Release
Shipping: Version, Install, Completion, Release
Overview
A CLI users trust has a clear version, easy install, optional completion, and reproducible releases. Most of this works with stdlib + Go toolchain; GoReleaser and package managers finish the last mile.
Version command / flag
var (
version = "dev"
commit = "none"
date = "unknown"
)
// go build -ldflags "-X main.version=1.0.0 -X main.commit=abc -X main.date=2026-01-01"var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("mytool %s (%s) built %s\n", version, commit, date)
},
}Also support mytool --version via Cobra rootCmd.Version.
From build info (no ldflags)
import "runtime/debug"
func versionString() string {
if bi, ok := debug.ReadBuildInfo(); ok {
return bi.Main.Version // often "(devel)" for go run
}
return "dev"
}Use ldflags for release binaries; buildinfo helps modules.
Install paths
# module install
go install example.com/mytool/cmd/mytool@v1.2.3
# local
go build -o dist/mytool ./cmd/mytool
sudo cp dist/mytool /usr/local/bin/Document GOBIN / PATH issues on macOS/Linux.
Man pages and docs
- Cobra: community tools generate man/markdown from command tree
- At minimum:
mytool --helpand README examples with copy-paste commands
Completions (install notes)
# zsh example
mytool completion zsh > "${fpath[1]}/_mytool"
# or
mytool completion zsh > ~/.zsh/completions/_mytool
compinit# bash
mytool completion bash | sudo tee /etc/bash_completion.d/mytoolDocument one path per shell; link to Cobra chapter 312.
Cross-compile
GOOS=linux GOARCH=amd64 go build -o mytool-linux-amd64 ./cmd/mytool
GOOS=darwin GOARCH=arm64 go build -o mytool-darwin-arm64 ./cmd/mytool
GOOS=windows GOARCH=amd64 go build -o mytool-windows-amd64.exe ./cmd/mytoolCGO_ENABLED=0 for static-ish pure Go binaries (usual for CLIs). On Linux that means no libc on the target: the same file runs on Debian, Alpine, and NixOS. Leave cgo on and a Ubuntu-built CLI can fail on Alpine with not found because the glibc dynamic linker is missing. See Go as a Systems Language.
Makefile sketch
VERSION ?= $(shell git describe --tags --always --dirty)
LDFLAGS := -s -w -X main.version=$(VERSION)
.PHONY: build test install
build:
go build -ldflags "$(LDFLAGS)" -o bin/mytool ./cmd/mytool
test:
go test ./...
install: build
install -m 755 bin/mytool $(HOME)/bin/mytool
GoReleaser (overview)
GoReleaser automates multi-arch builds, checksums, changelog, GitHub releases, Homebrew taps, etc.
# .goreleaser.yaml (sketch)
builds:
- main: ./cmd/mytool
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}}
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]# tag and release in CI
git tag v1.2.3 && git push --tagsDocker (optional for CLIs)
Often unnecessary for pure Go CLIs; useful for hermetic CI:
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/mytool ./cmd/mytool
FROM gcr.io/distroless/static
COPY --from=build /out/mytool /mytool
ENTRYPOINT ["/mytool"]Checksums and supply chain
sha256sum mytool-* > checksums.txt
# cosign / slsa later for high-security toolsPublish checksums next to release assets.
Rules of thumb
| Do | Don’t |
|---|---|
| Embed version in binary | Rely on filename alone |
| Provide install one-liners | Assume go install only |
| Ship completion for public tools | Require manual flag memorization |
| CGO_ENABLED=0 for CLIs | Accidental cgo → fragile cross-build |
Try next
- Add ldflags version to a sample tool; verify
mytool version. - Cross-compile for linux/amd64 from your Mac/Linux host.
- Generate completion and smoke-test tab complete for a subcommand.