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
+69
View File
@@ -0,0 +1,69 @@
# AGENTS.md — internal/wire (ADDRESSES)
**How this project addresses one repository and one issue, and nothing else.**
Two types, `Repo` and `Key`, and the parsing that reads them.
**Imports the standard library and nothing else** — no HTTP, no filesystem, no
configuration, no SDK, and above all not [`issue`](../issue/AGENTS.md). An
identifier that reached for any of those would drag every user of it into that
layer. Two tests hold it, see [`internal/AGENTS.md`](../AGENTS.md).
| file | what is in it |
|---|---|
| `key.go` | `Repo`, `Key`, `ParseRepo`, `ParseKey`, `Key.In`, and the `String` methods |
| `key_test.go` | every spelling above, and what a malformed one answers |
| `layering_test.go` | the two tests that keep this package at the bottom |
## Four spellings, one address
```go
wire.ParseKey("42") // Key{Number: 42} — repo zero: "this project's"
wire.ParseKey("#42") // the same, copied out of a body
wire.ParseKey("owner/repo#42") // qualified, out of the ledger
wire.ParseKey("https://git.example.com/owner/repo/issues/42")
```
All four because all four are what somebody has in hand — a number from a receipt,
a `#42` copied out of an issue body, a qualified key out of the ledger, a URL pasted
from a browser. Refusing three of them buys nothing.
**`Key.Repo` is zero when the caller named a number and nothing else**, which is the
common case on a command line: `42` means "42 in this project's repository", and
which repository that is, is the client's business. `Key.In(repo)` fills it in.
`Repo.Zero()` requires both halves — half a name addresses nothing.
## Why a key is not a bare number
`Key` has to survive being written to a file and read back: it is what the
number → slug ledger is keyed by and what the `gitea:` metadata field holds. **A
number is ambiguous the moment a dependency lives in another repository, and
dependencies are allowed to.** So a key is a repository and a number, always, and
`String()` spells it `owner/repo#42`.
## Why this package still exists after the SDK
The JSON shapes used to live here too, because the transport and the bridge both
had to name a Gitea issue and neither may import the other. They are
`code.gitea.io/sdk/gitea`'s now.
**What the SDK has no answer for is addressing.** It takes an owner, a name and an
`int64`, and never parses. So the parsing stays, and so does the pair of types it
produces — the values that go into the ledger, into the `gitea:` field, and into
every receipt. The longer version of that history is in
[`internal/AGENTS.md`](../AGENTS.md).
## What does not belong here
Anything that *does* something with an address: fetching, storing, resolving a
repository from configuration. This package parses and prints. Callers are
[`gitea`](../gitea/AGENTS.md), [`mapping`](../mapping/AGENTS.md) and
[`cmd`](../cmd/AGENTS.md).
## Keeping this file true
- **Scope:** `key.go` and its tests — the two types and the spellings they accept.
- **Update it when** a spelling is added or dropped, a type gains a field, or the
zero-value meaning of `Key.Repo` changes.
- **Do not** add a third type here without an argument for why it is an *address*.
Anything that is a payload belongs to the SDK; anything that is a fact about work
belongs to the domain.