01fb5a2703
There is no CI: the instance has no act_runner and none is planned, so releases are cut by hand. That makes `make check` the only thing standing between a mistake and the tracker, and it is one command: gofmt, vet, the suite with the cache defeated, `go mod verify`, a vendored build, and `kettle gen skills --check`. The last one is the invariant worth having — the plugin's SKILL.md command reference is generated from the binary's registry, so a flag that changed cannot ship with documentation that recommends the old one. `cli/cmd/release` publishes to Gitea using the same SDK the binary already vendors, which is a pleasing thing to be able to say: nothing third-party handles the artifacts. It is a second binary rather than a `kettle` subcommand on purpose — `kettle`'s command tree is what generates the plugin's skills, so a verb there ships to every operator, and publishing a release is build infrastructure. It is idempotent end to end: an existing release for the tag is reused, an asset of the same name is replaced rather than doubled, and a retried run converges instead of duplicating. `make release` refuses three things, each with its own message: a dirty working tree, a TAG that is not what `git describe` reports, and a tag the remote does not have. A release built from uncommitted code is unreproducible and nobody finds out until they need to reproduce it. `kettle version` reports the stamp, the toolchain and the VCS revision. The default is `dev`, and a hand build says so and means it — a binary out of somebody's working tree is not a release and must not claim to be one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
189 lines
7.7 KiB
Makefile
189 lines
7.7 KiB
Makefile
# 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
|
|
# plugin's documentation still matches, and -j would let the second start first.
|
|
.NOTPARALLEL:
|
|
|
|
MODULE := git.noodles.cam/claude-skills/marketplace/cli
|
|
SKILLS := ../plugins/kettle/skills
|
|
DIST := dist
|
|
BIN := $(DIST)/kettle
|
|
REMOTE ?= origin
|
|
|
|
# Where `make install` puts the binary. The plugin's skills expect `kettle` on
|
|
# PATH and say 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: everything the plugin's SKILL.md files say about
|
|
# a kettle command is generated from the registry the binary is built from, and
|
|
# this exits 1 the moment the two disagree. Run `kettle gen skills --out …`
|
|
# without --check to fix it.
|
|
gen-check: build
|
|
@echo 'docs gen skills --check'
|
|
@$(BIN) gen skills --out $(SKILLS) --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 the plugin's skills will not 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'
|