| name | go-makefile-writer |
| description | Canonical skill for Go Makefiles. Create/refactor root Makefiles for Go repositories with standardized build/test/lint/run targets, self-documenting help output, predictable artifacts, and maintainable target naming. |
| disable-model-invocation | true |
| allowed-tools | Read, Write, Grep, Glob, Bash(make*), Bash(go version*), Bash(go env*), Bash(go list*), Bash(go test*), Bash(go generate*), Bash(go install*), Bash(go get*), Bash(go build*), Bash(go mod*), Bash(go run*), Bash(go fmt*), Bash(git diff*), Bash(git status*), Bash(*discover_go_entrypoints.sh*) |
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
-
Select mode (Create or Refactor) and record rationale.
-
Inspect project structure:
- 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)
- 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)
- detect quality tools and conventions (
go test, golangci-lint, swag)
- detect code generation usage (
go generate, protobuf, wire, mockgen, etc.)
- detect containerization (
Dockerfile, docker-compose.yml)
- read
go.mod for Go version (go directive) and module path
- inspect existing
Makefile if present (Refactor mode)
- 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.
-
Plan target set:
- core targets:
help, fmt, tidy, test, cover, lint, clean
- version targets:
version (print embedded version info)
- CI target:
ci (fmt-check + lint + test + cover-check in one pass)
- optional targets:
swagger, generate, install-tools, test-integration, bench
- build targets:
build-all plus per-binary targets (with -ldflags version injection)
- run targets: per-binary
run-* targets
- container targets (when Dockerfile present):
docker-build, docker-push
- cross-compile targets (when needed):
build-linux, build-all-platforms
- Go version-aware decisions (see Go Version Awareness)
-
Compose and write root Makefile:
- keep targets explicit and predictable
- use variables (
GO, BIN_DIR, VERSION, COMMIT, BUILD_TIME) for repeated paths and build metadata
- inject version info via
-ldflags in all build targets
- include
.PHONY
- fail early with clear tool checks for optional dependencies
- use target templates from makefile-quality-guide.md
- Refactor mode: apply minimal-diff strategy and backward-compatibility rules from the mode definition above
-
Validate:
- run
make help
- run
make test
- run one representative
build-* target
- 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)
- if possible, run one representative
run-* target in a safe environment
- 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
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:
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:
- Mode:
Create or Refactor with rationale
- Project info: Go version (from
go.mod), layout (single-module or monorepo (N modules)), entrypoints discovered
- Changed files
- New/updated targets
- Deprecated/aliased targets (Refactor mode — list old name → new name mappings)
- Assumptions or missing tools
- 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