feat: publish releases with this repository's own SDK code
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>
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
# 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'
|
||||
Reference in New Issue
Block a user