Build automation
written in Go
Write ordinary Go functions. GoMake discovers them as runnable targets — with docs, arguments, namespaces, cross-package imports, and full testability.
// Build initializes the target with a [context.Context] for cancellation and a
// [ring.Ring] encapsulating I/O streams, environment variables, and arguments.
// Dependencies are injected explicitly to simplify testing.
func Build(ctx context.Context, rng *ring.Ring) error {
return exec.CommandContext(ctx, "go", "build", "./...").Run()
}$ gomake build
✓ done in 1.2s
$ gomake --list
build compiles the project
test runs the test suite
ci:lint runs golangci-lintWhy GoMake
Three reasons teams reach for it
One signature, plain Go, a compiled-and-cached binary. What sets GoMake apart comes down to three things.
Build automation written in Go
Shared across projects and people
//gomake:import — and go get -u ships a fix to all of them.Fully testable
ring.Ring. Call them directly in
tests, assert their output — no subprocess, no global state, no temp files.Sharing
Write a target once. The whole team runs it.
Build logic doesn’t have to live in one repo. Put your targets in a shared package, and every project imports them with one comment. Fix a bug once and go get -u propagates it to every repo and every teammate — no copy-paste, no drift.
Author once — a shared package
// Package lint holds the org's standard
// linting targets, shared across every repo.
package lint
// Lint runs the shared golangci-lint config.
func Lint(ctx context.Context, rng *ring.Ring) error {
cmd := exec.CommandContext(ctx,
"golangci-lint", "run")
cmd.Stdout = rng.Stdout()
cmd.Stderr = rng.Stderr()
return cmd.Run()
}
Import anywhere — one comment
//go:build gomake
package main
import (
// Merged into the root namespace.
_ "acme.dev/mk/lint" //gomake:import
// Prefixed under "release:".
_ "acme.dev/mk/release" //gomake:import release
)
Local and shared targets, side by side
$ gomake --list
build compiles the project # local
lint runs the shared golangci-lint config # imported
release:tag tags the next release # imported
release:publish publishes the build artifacts # imported
More
And the rest of the toolbox
Everything else GoMake gives you out of the box — no configuration required.
Builtin targets
makefile.go.Namespaces
ci:docker:build.Docs from comments
--help <target>.Cross-platform
GOOS and GOARCH control
compilation; go.work supported.Standalone binary
--bin to compile a self-contained binary for CI or Docker images that
don’t have gomake installed.Cached builds
Testing
Targets are plain functions — test them that way
Because all I/O flows through ring.Ring, you can call a target directly in a test, inject a buffer for stdout, set env vars, pass args — and assert the output. No subprocesses, no temp files, no global state.
makefile.go
func Greet(ctx context.Context, rng *ring.Ring) error {
name := rng.EnvGet("GREETER_NAME")
if name == "" {
name = "World"
}
if args := rng.Args(); len(args) > 0 {
name = args[0]
}
out := rng.Stdout()
_, err := fmt.Fprintf(out, "Hello, %s!\n", name)
return err
}
makefile_test.go
func TestGreet(t *testing.T) {
// --- Given ---
var out bytes.Buffer
rng := ring.New(
ring.WithArgs([]string{"Alice"}),
ring.WithEnv([]string{"GREETER_NAME=Bob"}),
)
rng.SetStdout(&out)
// --- When ---
err := Greet(t.Context(), rng)
// --- Then ---
assert.NoError(t, err)
// Argument value wins over environment variable.
assert.Equal(t, "Hello, Alice!\n", out.String())
}
At a glance
Everything GoMake gives you
One signature, plain Go, and a compiled-and-cached binary. Here is what you get out of the box.
| Capability | How GoMake does it |
|---|---|
| Target signature | func(ctx, *ring.Ring) error — one form |
| I/O access | rng.Stdout() / rng.Stderr() — injectable |
| Arguments | rng.Args() — the full []string |
| Environment | rng.EnvGet — isolated and seedable in tests |
| Testing | Call the target directly with a controlled Ring |
| Namespaces | Methods on //gomake:ns_root structs, nestable |
| Cross-package imports | //gomake:import comment tag |
| Built-in targets | targets.yaml — compiled into the binary |
| Binary cache | SHA-256 of sources + go.sum + version + arch |
| Progress feedback | Automatic, after 500 ms |
| Cross-platform | Go build tags + GOOS/GOARCH; go.work too |
| Standalone binary | gomake --bin ./make |
Ready to start?
Read the docs, learn to test your targets, or jump straight to the source.