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:
@@ -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.
|
||||
Reference in New Issue
Block a user