Every repo eventually grows a README section called "useful commands" that nobody keeps up to date. The lint invocation, the test flags, the four-line Docker incantation, the thing you have to run before the thing. They rot, because a README is documentation and documentation drifts from reality the moment it's written. I've started replacing that section with a Makefile, in projects that contain no C whatsoever, and it has been quietly excellent.
The pitch is simple. A Makefile is an executable list of named commands that lives next to the code. make test, make lint, make build, make fmt. The names are the documentation, they can't drift because they're what actually runs, and make is already on every machine you'll touch. No new dependency, no npm install to use the task runner.
Here is roughly what one looks like for a Go service of mine, though the language is beside the point:
.PHONY: help test lint fmt build clean
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
test: ## Run the tests
go test ./... -race -count=1
lint: ## Run the linter
golangci-lint run
fmt: ## Format the code
gofmt -w .
build: ## Build the binary
go build -o bin/app ./cmd/app
clean: ## Remove build artefacts
rm -rf bin/
The .PHONY line matters. It tells make these targets are not files, so it doesn't get confused and decide there's "nothing to do" because a file called test happens to exist. The little help target is the bit people like: every target with a ## comment shows up in make help, so the Makefile documents itself. New person joins, types make help, sees the whole vocabulary of the project in one screen.
Where make genuinely earns its keep over a scripts/ folder full of bash is dependencies between targets. If your integration tests need the binary built first, you write integration: build and make sorts out the ordering. You stop manually remembering to build before you test.
There are real sharp edges and I won't pretend otherwise. Tabs, not spaces, for the recipe indentation, and the error you get when you forget is famously unhelpful. Every line in a recipe runs in its own shell, so a cd on one line is gone by the next unless you join them with &&. And make's actual purpose, incremental rebuilds based on file timestamps, is wasted here because none of these are real file targets. That's fine. I'm not using it as a build system. I'm using it as a task runner that happens to be installed everywhere and that everyone half-recognises.
If a project gets genuinely complicated, sure, reach for Task or Just or whatever the team prefers. But for the common case of "I just want named commands that don't rot", a twenty-line Makefile does the job and asks for nothing in return. That's a good trade.