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
+43
View File
@@ -0,0 +1,43 @@
# AGENTS.md — cmd/kettle
The binary's entry point, and all of it:
```go
func main() { os.Exit(cmd.Main(os.Args[1:])) }
```
One file, `main.go`, holding a package comment and that line. The sibling
[`cmd/release`](../release/AGENTS.md) is the module's other binary — build
infrastructure, deliberately not a `kettle` verb.
## Why it is empty
Everything a `main` usually accumulates — flag parsing, dispatch, usage text,
error formatting, exit codes — is in [`internal/cmd`](../../internal/cmd/AGENTS.md),
where it is **testable**. A `main` package cannot be imported, so anything written
here can only be exercised by running the binary; the command tree is instead a
library with one caller, and its tests run it as a subprocess *and* call into it
directly where that is cheaper.
The exit status is the only thing this layer owns, and it owns it because
`os.Exit` skips deferred functions: it has to happen after everything else is
finished, at the outermost frame, and nowhere else in the tree may call it.
The version a build reports is **not** stamped here either. `-ldflags -X` names
`internal/cmd.Version`, because that is where the `version` command reads it and
where a test can build with the flag and read the answer back — a `-X` whose symbol
path is one character wrong is silently ignored, and the binary goes on saying `dev`.
## Adding a command
Nothing here changes. A new command is a `register(&Command{…})` in an `init()`
over in [`internal/cmd`](../../internal/cmd/AGENTS.md) — that is the whole point
of a registry.
## Keeping this file true
- **Scope:** `main.go`, and the reason it stays this short.
- **Update it when** this package grows a second file or a line that does anything
but delegate — which should be treated as a design change and argued for, not
documented after the fact.
- **Do not** describe commands, flags or exit codes here.