Shipping: Version, Install, Completion, Release

Updated

September 8, 2026

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 --help and 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/mytool

Document 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/mytool

CGO_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 --tags

Docker (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 tools

Publish 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

  1. Add ldflags version to a sample tool; verify mytool version.
  2. Cross-compile for linux/amd64 from your Mac/Linux host.
  3. Generate completion and smoke-test tab complete for a subcommand.