# The gate, and the release, for a repository with no CI. # # There is no act_runner on the instance this lives on and none is planned, so # NOTHING RUNS ON A PUSH. `make check` is the only thing standing between a # mistake and the tracker, and it is on whoever is committing to run it — the # same five steps a workflow would have run, in one command, exiting non-zero # the moment one of them fails. # # `make release` is the other half: a release is cut by hand, from a developer's # machine, by this module's own code (cmd/release) talking to Gitea's API. It # refuses to publish a dirty tree or a tag that is not the version it is about # to stamp into the binaries, because a release built from uncommitted code is # unreproducible and nobody finds out until they need to reproduce it. SHELL := /bin/sh # Sequential on purpose: `check` builds the binary and then asks it whether the # documents embedded in it still match its own command registry, and -j would # let the second start first. .NOTPARALLEL: MODULE := git.noodles.cam/claude-skills/marketplace/cli ASSETS := internal/scaffold/assets DIST := dist BIN := $(DIST)/kettle REMOTE ?= origin # Where `make install` puts the binary. Everything kettle writes into a project # expects `kettle` on PATH and says so when it is not; override for a Go-style # layout: # make install BINDIR=$(go env GOPATH)/bin BINDIR ?= $(HOME)/.local/bin # VERSION is what a binary reports for `kettle version`. It is derived from git # rather than kept in a file: a number somebody has to remember to bump is a # number that will be wrong. A tree that is not on a tag says so, and a dirty # tree says that too — "v0.2.0-4-g1a2b3c4-dirty" is the honest answer, and it is # exactly what you want to see in a bug report. VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo dev) LDFLAGS := -X $(MODULE)/internal/cmd.Version=$(VERSION) PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 .PHONY: help check fmt vet test verify build gen-check dist release install clean help: @echo 'kettle — there is no CI here; these are what a person runs.' @echo @echo ' make check the gate: fmt, vet, test, modules, build, docs' @echo ' make build dist/kettle, version stamped' @echo ' make install [BINDIR=…] build straight onto your PATH' @echo ' make dist cross-compile every platform + SHA256SUMS' @echo ' make release TAG=v1.2.3 [NOTES=notes.md] [TITLE="…"]' @echo ' dist, then publish it to Gitea' @echo ' make clean remove dist/' @echo @echo " version $(VERSION)" @echo " bindir $(BINDIR)" # --------------------------------------------------------------------------- # the gate # --------------------------------------------------------------------------- check: fmt vet test verify build gen-check @echo 'check all clear — $(VERSION)' # gofmt reports rather than rewrites: a formatting change made silently by a # build is a change nobody reviewed. cmd and internal, never the module cache. fmt: @out=`gofmt -l cmd internal`; \ if [ -n "$$out" ]; then \ echo 'gofmt these files are not formatted:'; \ echo "$$out" | sed 's/^/ /'; \ echo ' run: gofmt -w cmd internal'; \ exit 1; \ fi; \ echo 'gofmt clean' vet: @echo 'vet go vet ./...' @go vet ./... # -count=1 defeats the test cache. A gate that can pass because it passed an # hour ago on different code is not a gate. test: @echo 'test go test -count=1 ./...' @go test -count=1 ./... # Two checks, because they answer different questions. `go mod verify` says the # module cache matches go.sum; the vendored build says the committed vendor/ is # complete and is what actually compiles. A vendor/ that has drifted from go.mod # fails nothing until somebody builds on a machine with a cold cache. verify: @echo 'modules go mod verify' @go mod verify @echo 'vendor go build -mod=vendor ./...' @go build -mod=vendor ./... build: @mkdir -p $(DIST) @go build -trimpath -ldflags '$(LDFLAGS)' -o $(BIN) ./cmd/kettle @echo 'build $(BIN) — $(VERSION)' # The documentation invariant, and it is circular on purpose. The documents an # operator gets are embedded in the binary, and the flag tables inside them are # rendered from the registry that same binary is built from — so the embedded # copy has to already hold what the current registry would produce. This exits 1 # the moment it does not; `$(BIN) gen scaffold --out $(ASSETS)` fixes it, and # then the binary has to be rebuilt to pick the new bytes up. gen-check: build @echo 'docs gen scaffold --check' @$(BIN) gen scaffold --out $(ASSETS) --check # --------------------------------------------------------------------------- # building for other people # --------------------------------------------------------------------------- install: @mkdir -p $(BINDIR) @go build -trimpath -ldflags '$(LDFLAGS)' -o $(BINDIR)/kettle ./cmd/kettle @echo 'installed $(BINDIR)/kettle — $(VERSION)' @case ":$$PATH:" in \ *":$(BINDIR):"*) ;; \ *) echo "note: $(BINDIR) is not on your PATH, so nothing kettle writes will find it" ;; \ esac # CGO_ENABLED=0 because these binaries are downloaded by people whose machines # are not this one: a build that links against the host's libc is a build that # runs on the host. dist: @rm -f $(DIST)/kettle_* $(DIST)/SHA256SUMS @mkdir -p $(DIST) @for p in $(PLATFORMS); do \ os=$${p%/*}; arch=$${p#*/}; \ out=$(DIST)/kettle_$(VERSION)_$${os}_$${arch}; \ CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \ go build -trimpath -ldflags '$(LDFLAGS)' -o $$out ./cmd/kettle || exit 1; \ echo "dist $$out"; \ done @cd $(DIST) && \ if command -v sha256sum >/dev/null 2>&1; then \ sha256sum kettle_$(VERSION)_* > SHA256SUMS; \ else \ shasum -a 256 kettle_$(VERSION)_* > SHA256SUMS; \ fi @echo 'dist $(DIST)/SHA256SUMS' # --------------------------------------------------------------------------- # cutting one # --------------------------------------------------------------------------- # Published by cmd/release, which is this module's own code against Gitea's own # API — no third-party action, nothing between the tag and what people download # that is not in this repository. # # The three refusals are the point of doing it here rather than by hand: # # dirty tree what shipped would not be what is committed, and nobody could # rebuild it; # wrong tag TAG must be the version `git describe` reports, which is only # true when the tag exists and HEAD is standing on it — so this # also catches "I forgot to tag" and "I tagged, then committed"; # unpushed tag a release naming a tag the server does not have is a download # page pointing at a commit nobody else can fetch. release: @test -n '$(TAG)' || { echo 'usage: make release TAG=v1.2.3 [NOTES=notes.md] [TITLE="…"]'; exit 2; } @if [ -n "`git status --porcelain`" ]; then \ echo 'refusing: the working tree is dirty — a release built from uncommitted code cannot be rebuilt'; \ git status --short | sed 's/^/ /'; \ exit 1; \ fi @if [ '$(VERSION)' != '$(TAG)' ]; then \ echo 'refusing: TAG is $(TAG) but this commit describes as $(VERSION)'; \ echo ' the binaries would be stamped $(VERSION) and the release would claim $(TAG).'; \ echo ' tag this commit first: git tag -a $(TAG) -m $(TAG)'; \ exit 1; \ fi @if ! git ls-remote --exit-code --tags $(REMOTE) 'refs/tags/$(TAG)' >/dev/null 2>&1; then \ echo 'refusing: $(TAG) is not on $(REMOTE) — push it first: git push $(REMOTE) $(TAG)'; \ echo ' (another remote? make release TAG=$(TAG) REMOTE=…)'; \ exit 1; \ fi @$(MAKE) dist @echo 'release publishing $(TAG) with cmd/release' @go run ./cmd/release --tag '$(TAG)' \ $(if $(TITLE),--title '$(TITLE)') \ $(if $(NOTES),--notes-file '$(NOTES)') \ $(DIST)/kettle_$(VERSION)_* $(DIST)/SHA256SUMS clean: @rm -rf $(DIST) @echo 'clean $(DIST) removed'