Skip to content

Go Makefile Writer

Design a practical root Makefile that is readable, reproducible, and aligned with repository layout.

Quick Reference

If you need to… Go to
Create a Makefile from scratch for a new Go project §Execution Modes → Create + §Workflow
Refactor or update an existing Makefile (minimal-diff) §Execution Modes → Refactor
Decide which targets to include (build, test, lint, ci…) §Workflow (Plan targets)
Get a complete working Makefile example to start from Load references/golden/simple-project.mk or complex-project.mk
Check quality rules, variable conventions, .PHONY requirements Load references/makefile-quality-guide.md
Review a Makefile PR quickly Load references/pr-checklist.md
Handle a monorepo or multi-module Go repo §Monorepo Support

Execution Modes

Select a mode before starting and state it in the output report.

Create (new Makefile from scratch)

Refactor (modify existing Makefile)

  • Minimal-diff edits — change only what is needed; do not rewrite the entire file.
  • Backward compatibility: if target names change, keep aliases for at least one transition period and document them in the output report.
  • Preserve existing useful targets unless user explicitly asks to remove them.
  • Before editing, snapshot the current target list via make -qp | awk -F: '/^[a-zA-Z0-9_-]+:/ {print $1}' | sort -u for comparison.
  • Validation must include verifying that previously used critical targets still work (or their aliases do).

Workflow

  1. Select mode (Create or Refactor) and record rationale.

  2. Inspect project structure:

  3. discover cmd/**/main.go entrypoints by running this skill's discovery script against the target repo: bash <skill-dir>/scripts/discover_go_entrypoints.sh <project-root> (the script lives in the skill directory, not in the target repo — pass the repo root as its argument)
  4. if the script cannot run, fall back to find cmd -name main.go -type f (or rg --files cmd | grep '/main\.go$' when rg is available)
  5. detect quality tools and conventions (go test, golangci-lint, swag)
  6. detect code generation usage (go generate, protobuf, wire, mockgen, etc.)
  7. detect containerization (Dockerfile, docker-compose.yml)
  8. read go.mod for Go version (go directive) and module path
  9. inspect existing Makefile if present (Refactor mode)
  10. detect workspace / multi-module layout via the toolchain first: go env GOWORK (a non-empty path means a go.work workspace → its modules are go list -m). Only when there is no go.work fall back to a scoped go.mod search (bash <skill-dir>/scripts/discover_go_entrypoints.sh --modules <project-root>, which excludes vendor/, testdata/, examples/). See §Monorepo Support.

  11. Plan target set:

  12. core targets: help, fmt, tidy, test, cover, lint, clean
  13. version targets: version (print embedded version info)
  14. CI target: ci (fmt-check + lint + test + cover-check in one pass)
  15. optional targets: swagger, generate, install-tools, test-integration, bench
  16. build targets: build-all plus per-binary targets (with -ldflags version injection)
  17. run targets: per-binary run-* targets
  18. container targets (when Dockerfile present): docker-build, docker-push
  19. cross-compile targets (when needed): build-linux, build-all-platforms
  20. Go version-aware decisions (see Go Version Awareness)

  21. Compose and write root Makefile:

  22. keep targets explicit and predictable
  23. use variables (GO, BIN_DIR, VERSION, COMMIT, BUILD_TIME) for repeated paths and build metadata
  24. inject version info via -ldflags in all build targets
  25. include .PHONY
  26. fail early with clear tool checks for optional dependencies
  27. use target templates from makefile-quality-guide.md
  28. Refactor mode: apply minimal-diff strategy and backward-compatibility rules from the mode definition above

  29. Validate:

  30. run make help
  31. run make test
  32. run one representative build-* target
  33. build, then run the binary's --version to verify injection reached the artifact (make version only prints the Make variables, not what the binary embeds)
  34. if possible, run one representative run-* target in a safe environment
  35. Refactor mode: verify previously used critical targets still work (or provide aliases); compare target list before vs after

Rules

Target Design

  • Prefer explicit targets over complex metaprogramming unless the user asks for DRY-heavy style.
  • Keep artifact outputs deterministic (under bin/).
  • Keep help output self-documenting via ## comments with .DEFAULT_GOAL := help.
  • Map target names to cmd/ path semantics: cmd/<name>build-<name>, cmd/<kind>/<name>build-<kind>-<name>.
  • Declare all non-file targets in .PHONY.
  • Output executable bare Makefile by default (tabs for recipes, not spaces).

Build Quality

  • Inject version metadata via -ldflags in build targets:
    LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildTime=$(BUILD_TIME)
    
    -X only sets an existing package-level string var — -X main.version assumes var version string in package main; discover the real package/name first and use its import path if it lives elsewhere (a wrong path silently no-ops). -s -w strips the symbol table and DWARF (release builds only; keep a debug build without it).
  • Default the test target to -race — it catches real data races. But -race requires CGO_ENABLED=1, a supported OS/arch, and adds ~5–10× memory / 2–20× time. Offer a genuine race-free equivalent, test-norace (go test ./... — the full suite, no race), for cgo-disabled builds and platforms without race support. Do not conflate this with test-short (go test -short ./...): -short skips testing.Short()-gated cases, so it runs a smaller set — it turns off race only as a side effect and is not a substitute for the full suite.
  • CGO_ENABLED=0 is the default for pure-Go static builds and containers. For cgo projects (import "C", mattn/go-sqlite3, …) keep CGO_ENABLED=1 — and note the two collide: a -race test target cannot run under CGO_ENABLED=0.
  • For reproducible release builds add -trimpath (strips local paths so the binary is checkout-location-independent) and drive buildTime from SOURCE_DATE_EPOCH. This raises reproducibility but is not absolute: git describe --dirty makes VERSION depend on tree state, so only a clean checkout with a fixed toolchain is bit-for-bit reproducible.
  • Pin tool versions in install-tools for CI reproducibility.

Safety

  • Fail early with clear tool-presence checks (command -v <tool>) for optional dependencies.
  • ci target must mirror actual CI pipeline — developers should catch issues before push.
  • Check for code generation staleness (generate-check) when go generate is used.

Go Version Awareness

Read go.mod for the go directive before composing the Makefile. Record as Go version: X.Y in the output report.

Go Version Makefile Impact
< 1.16 go install does not support pkg@version; use go get for tool installation
≥ 1.18 Go workspaces (go.work) and fuzzing (go test -fuzz) available
≥ 1.20 go build -cover + GOCOVERDIR enable whole-program / integration coverage; consider a cover-integration target
≥ 1.21 go.mod toolchain directive (automatic toolchain selection); built-in min/max/clear
≥ 1.22 Per-iteration loop variable semantics; no Makefile impact but note in output

If go.mod is not found or not readable, record Go version: unknown and use conservative defaults (no version-specific features).

Monorepo Support

When the repo is a Go workspace or multi-module layout (step 1 Inspect), adapt for monorepo:

  • Detect via the toolchain, not a bare file search: prefer go.work (go env GOWORK); when it exists, the modules are its use directives (go list -m run inside the workspace). Only fall back to searching for go.mod files when there is no go.work, and then exclude vendor/, testdata/, examples/, and tool-only modules.
  • Per-module targets: generate test-<module>, lint-<module>, build-<module> for each module that has entrypoints
  • Aggregate targets: test-all, lint-all, build-all that iterate over the workspace modules
  • Per-module go mod tidy: tidy operates on a single main module — run it inside each module (for m in $(MODULES); do (cd $$m && go mod tidy); done), never once at the workspace root
  • Root Makefile pattern:
# Workspace-aware: use go.work's module list when present, else a SCOPED go.mod
# search (a bare `rg go.mod` sweeps in vendor/testdata/examples).
GOWORK := $(shell go env GOWORK 2>/dev/null)
ifeq ($(GOWORK),)
MODULES := $(shell rg --files -g 'go.mod' 2>/dev/null | xargs -I{} dirname {} | grep -Ev '(^|/)(vendor|testdata|examples?)(/|$$)' | sort)
else
MODULES := $(shell go list -m -f '{{.Dir}}' 2>/dev/null)
endif

test-all: ## Run tests for all modules
    @for mod in $(MODULES); do \
        echo "=== testing $$mod ==="; \
        (cd $$mod && go test -race ./...) || exit 1; \
    done

lint-all: ## Lint all modules
    @for mod in $(MODULES); do \
        echo "=== linting $$mod ==="; \
        (cd $$mod && golangci-lint run) || exit 1; \
    done
  • When the project is a single-module repo, this section does not apply — use the standard single-module workflow.
  • Record Layout: monorepo (N modules) or Layout: single-module in the output report.

Anti-Patterns (DO NOT generate these)

Before writing or reviewing a Makefile, check against these common mistakes. If your output matches any of these patterns, fix it before delivering.

Missing fundamentals: - No help target or missing ## self-documenting comments - No .PHONY declaration for non-file targets - No race testing at all — the default test should use -race; provide test-norace (full suite, no race) as the cgo-off / unsupported-platform equivalent. test-short runs a smaller quick set and is not a substitute. - No -ldflags version injection in build-* targets

Naming and layout: - Target names not matching cmd/ path semantics (e.g., cmd/consumer/sync but target is build-sync instead of build-consumer-sync) - run-* targets leaving ad-hoc binaries in source directories instead of bin/

Reproducibility: - install-tools using @latest for all tools in CI (pin specific versions for reproducibility; @latest is acceptable only for local dev convenience) - Hardcoding a tool version without discovering the repo's existing pin — check CI workflows, .golangci.version / .tool-versions (asdf/mise), and any existing install-tools first; use a current compatible version (golangci-lint is now v2, module path .../v2/cmd/golangci-lint) and prefer the tool's official installer where it documents one (go install from source is explicitly not guaranteed for golangci-lint) - ci target that diverges from the actual CI pipeline — make ci should mirror CI exactly - Hidden assumptions about local paths or OS-specific tools (e.g., sed -i without considering macOS vs GNU differences)

Cross-compilation: - Cross-compiling a pure-Go binary without CGO_ENABLED=0 — produces dynamically linked binaries that fail on target machines (cgo projects instead need CGO_ENABLED=1 and a cross C toolchain) - Hardcoded GOOS/GOARCH without variable override

Code generation: - Generated code not checked for staleness before build (missing generate-check target)

Over-engineering: - Overly dynamic Make metaprogramming (eval/call/define) that reduces readability when explicit targets would be clearer - Tab-vs-space issues in Makefile recipes (recipes MUST use tabs, not spaces)

Quality Improvements to Offer

  • Add .DEFAULT_GOAL := help.
  • Add cover-check target with a configurable threshold.
  • Add tidy target for go mod tidy + go mod verify.
  • Keep run-* from polluting source directories; prefer go run ./cmd/... or run from bin/.
  • Pin tool versions in install-tools for CI reproducibility (see quality-guide §11).

Load References Selectively

When starting any Makefile creation or refactor task: → Run this skill's discovery script against the target repo first — bash <skill-dir>/scripts/discover_go_entrypoints.sh <project-root> — to discover cmd/**/main.go binary locations and infer project shape (single-binary vs multi-binary). Add --modules to list workspace/multi-module directories.

When writing or reviewing specific targets (build, test, lint, run, install-tools), or checking quality rules: → Load references/makefile-quality-guide.md for canonical target templates, variable conventions, .PHONY rules, self-documenting help output, and the 15-item review checklist.

When reviewing a PR that touches a Makefile: → Load references/pr-checklist.md for the fast Makefile-specific PR review checklist (target naming, portability, idempotency, CI compatibility).

When you need a complete working Makefile as a starting point or reference: → Load references/golden/simple-project.mk for a single-binary project with minimal tooling. → Load references/golden/complex-project.mk for a multi-binary project with Docker, code generation, and cross-compilation targets.

Output Contract

When generating or refactoring a Makefile, always return:

  1. Mode: Create or Refactor with rationale
  2. Project info: Go version (from go.mod), layout (single-module or monorepo (N modules)), entrypoints discovered
  3. Changed files
  4. New/updated targets
  5. Deprecated/aliased targets (Refactor mode — list old name → new name mappings)
  6. Assumptions or missing tools
  7. Validation commands executed with pass/fail status

Example Output (Create mode, single-binary)

### Mode
Create — no existing Makefile found

### Project info
- Go version: 1.23 (from go.mod)
- Layout: single-module
- Entrypoints: cmd/api

### Changed files
- `Makefile` (created)

### New targets
help, build-api, build-all, run-api, fmt, fmt-check, tidy,
test, cover, cover-check, lint, ci, version, install-tools,
check-tools, clean

### Deprecated/aliased targets
(none — new Makefile)

### Assumptions
- golangci-lint will be installed via `make install-tools`
- Version info injected into `main.version`, `main.commit`, `main.buildTime`

### Validation
✓ make help       — 16 targets listed
✓ make test       — all tests pass with -race
✓ make build-api  — binary at bin/api
✓ ./bin/api --version — version=v0.1.0-dirty commit=abc1234 buildTime=… (injection reached the artifact)

Self-Validation

Run scripts/run_regression.sh to verify skill integrity: - Contract tests: structure of SKILL.md, quality guide, golden examples, discovery script - Golden review tests: all defect/FP fixtures' rules covered in docs - Coverage matrix: see scripts/tests/COVERAGE.md