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
+93
View File
@@ -0,0 +1,93 @@
package cmd
import (
"flag"
"fmt"
"runtime"
"runtime/debug"
)
// Version is what this binary was built as. It is stamped at link time and
// defaults to something honest.
//
// "dev" is the truth for a build from a working tree: a binary somebody built
// out of a checkout is not a release and must not claim to be one. A release
// build says otherwise by naming this variable:
//
// go build -ldflags "-X git.noodles.cam/claude-skills/marketplace/cli/internal/cmd.Version=v1.2.3" ./cmd/kettle
//
// which is what `make build`, `make dist` and `make release` do. The path is
// exercised by a test that builds with the flag and reads the answer back,
// because a -X whose symbol path is one character wrong is silently ignored and
// the binary goes on reporting "dev".
var Version = "dev"
func init() {
register(&Command{
Name: "version",
Group: GroupProject,
Short: "print the version this binary was built as",
Long: `A binary that cannot say what it is, is a support problem: an operator with an
old copy on PATH and a skill written against a newer one has no way to see the
mismatch, and neither does anybody reading their transcript.
The version is stamped at link time. A build from source says "dev" and means
it — that is not a placeholder to be edited, it is the answer for a binary that
came out of somebody's working tree rather than off a tag.
The commit is reported when the build recorded one, which ` + "`go build`" + ` does from
git and a build from an unpacked tarball cannot. A tree with uncommitted
changes in it says so beside the commit.`,
Examples: []Example{
{"kettle version", "the version, the toolchain and the commit"},
{"kettle version --short", "just the version, for a script"},
},
Setup: func(fs *flag.FlagSet) func([]string) error {
short := fs.Bool("short", false, "print the version alone, with nothing around it")
return func(args []string) error {
if len(args) > 0 {
return Fail("version takes no arguments")
}
if *short {
fmt.Println(Version)
return nil
}
fmt.Printf("kettle %s\n", Version)
fmt.Printf("built %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
if rev := revision(); rev != "" {
fmt.Printf("commit %s\n", rev)
}
return nil
}
},
})
}
// revision is the commit this binary was built from, or "" when the build
// recorded none.
//
// `go build` stamps it out of git; a build from an unpacked tarball has no
// repository to ask, and there is nothing to report rather than something to
// invent. A dirty tree is named as one: the commit is then a lower bound on
// what is in the binary and not a description of it.
func revision() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
var rev string
var dirty bool
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
rev = s.Value
case "vcs.modified":
dirty = s.Value == "true"
}
}
if rev != "" && dirty {
rev += " (with uncommitted changes)"
}
return rev
}