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:
naudachu
2026-08-12 01:01:27 +05:00
parent ec0a1893b7
commit 01fb5a2703
27 changed files with 3385 additions and 266 deletions
+38
View File
@@ -491,6 +491,44 @@ func TestAMissingLoginIsExplained(t *testing.T) {
}
}
// The version is "dev" until a build stamps it, and the STAMPING is what is
// tested here rather than the printing.
//
// A `-X` whose symbol path is one character wrong is not an error: the linker
// ignores it and the binary goes on reporting "dev" for the rest of its life,
// which is discovered by an operator holding a release that will not say what
// it is. So this builds with the flag the Makefile uses and reads the answer
// back out of the binary.
func TestVersionSaysDevUntilABuildStampsIt(t *testing.T) {
dir := t.TempDir()
r := mustRun(t, dir, "version")
if !strings.Contains(r.stdout, "dev") || !strings.Contains(r.stdout, "built") {
t.Errorf("a build from source must say what it is:\n%s", r.out())
}
// A version needs no project: it is a fact about the binary, and the
// question is asked most often by somebody whose project is not resolving.
if short := mustRun(t, dir, "version", "--short"); strings.TrimSpace(short.stdout) != "dev" {
t.Errorf("--short printed %q, want dev", short.stdout)
}
const stamp = "v9.9.9-from-the-test"
stamped := filepath.Join(t.TempDir(), "kettle")
build := exec.Command("go", "build",
"-ldflags", "-X git.noodles.cam/claude-skills/marketplace/cli/internal/cmd.Version="+stamp,
"-o", stamped, "../../cmd/kettle")
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("building a stamped binary: %v\n%s", err, out)
}
out, err := exec.Command(stamped, "version", "--short").Output()
if err != nil {
t.Fatalf("running the stamped binary: %v", err)
}
if got := strings.TrimSpace(string(out)); got != stamp {
t.Errorf("the stamped binary reports %q, want %q — the -X symbol path is wrong", got, stamp)
}
}
func closeIssue(t *testing.T, path string) {
t.Helper()
setField(t, path, "state", "closed")