Files
marketplace/cli/internal/AGENTS.md
T
naudachu 01fb5a2703 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>
2026-08-12 01:01:27 +05:00

88 lines
5.2 KiB
Markdown

# AGENTS.md — internal/, and the boundaries between the packages in it
Seven packages, one direction of knowledge. The diagram is in
[`cli/AGENTS.md`](../AGENTS.md); **this file owns the rules that hold it and the
tests that fail when one is broken.** Each package's own document owns what is
inside it.
The rule in one sentence: **read the diagram bottom-up and each layer knows
strictly less about trackers than the one above it.** A tracker concept — an issue
number, a login, an HTTP call, a label colour — that shows up in
[`issue`](issue/AGENTS.md) is in the wrong place, and a domain concept — a
section, an acceptance criterion, a type taxonomy — that shows up in
[`gitea`](gitea/AGENTS.md) is in the wrong place too.
## Four rules, seven tests
Each test fails on a real mistake rather than on a naming convention.
| rule | enforced by |
|---|---|
| [`issue`](issue/AGENTS.md) may import [`project`](project/AGENTS.md) and the standard library, and **nothing else** | `TestDomainDependsOnNothing` walks `go list -deps` and fails on any import path with a dot in its first element — which is what keeps yaml *and* the SDK out of the domain; `TestDomainDoesNotReachTheNetworkOrTheShell` names `net/http`, `net`, `os/exec` and `encoding/json`, standard library the first test cannot catch |
| [`wire`](wire/AGENTS.md) imports **only** the standard library | `TestWireDependsOnNothing` and `TestWireReachesNeitherTheNetworkNorTheDisk`, the same two checks |
| [`gitea`](gitea/AGENTS.md) must not import [`issue`](issue/AGENTS.md) **or** [`mapping`](mapping/AGENTS.md) | `TestTransportDoesNotImportTheDomain` — the transport knows numbers, logins, HTTP and JSON, and none of what they mean |
| [`mapping`](mapping/AGENTS.md) reaches for nothing but the domain, `wire` and the SDK, and does no I/O | `TestTheBridgeTranslatesAndNothingElse` on its **direct** imports, with `os`, `net/http`, `internal/gitea`, `internal/config` and `internal/project` named; `TestTheBridgeHasNoClock` greps its sources for `time.Now` |
The domain's two tests were **untouched by the migration to the Gitea SDK, and
that is the point: the domain did not notice it happened.**
## The one rule that got weaker, and why the trade was taken
The payload shapes used to live in `wire`, a package that imported the standard
library and nothing else, so "the bridge cannot reach a transport" was a fact
about the import graph: there was nothing in its dependency closure that could
open a socket. `code.gitea.io/sdk/gitea` is a client and a set of types in one
package, so importing the types imports the client, and a test that walked the
closure would now be asserting something false.
What is still true, and what the test now says, is that **mapping performs no
I/O** — no `os`, no `net/http`, no transport, no configuration, no clock. Note
the deliberate asymmetry with the domain's test: this one checks **direct**
imports, because the domain reaches `os` *through* `project` and that is the
domain's business. `time` is allowed here where it was not, because the SDK hands
over a `time.Time` and somebody has to format it back into the string an issue
file holds; the clock itself is still the caller's, and the grep for `time.Now`
is what says so.
## Why `wire` still exists
It existed because Go needs the JSON shapes to be one type — the transport and
the bridge were written in parallel and each invented its own `Issue`, `Label`,
`Milestone` and `Comment`. The SDK settles that argument for the shapes.
**What survives is addressing**, which the SDK has no answer for at all: it takes
an owner, a name and an `int64`, and never parses. `42`, `#42`, `owner/repo#42`
and an issue URL are four spellings of one address, all four are what somebody
has in hand, and `wire.Key` is what the ledger is keyed by and what the `gitea:`
metadata field holds. So `wire` keeps `Repo`, `Key`, their parsing and their
tests, and lost the payloads.
## Adding a package here
Three questions, in order:
1. **What does it know that its neighbours must not?** A package that cannot
answer this is a file in an existing one.
2. **Which direction does it import?** Draw it into the diagram in
[`cli/AGENTS.md`](../AGENTS.md) before writing code; an arrow that has to point
both ways means the split is in the wrong place.
3. **What test fails when the boundary is crossed?** Write it with the package,
not after. Every rule above has one, and each of them exists because the
equivalent convention in the Python version was a grep somebody eventually
forgot to run.
Then give it an `AGENTS.md`, add it to the table in [`cli/AGENTS.md`](../AGENTS.md),
and add its rule to the table above.
## Keeping this file true
- **Scope:** the boundaries *between* the packages under `internal/` — the four
rules, the seven tests that hold them, and the history of the one that changed.
Files: every `layering_test.go`, plus `TestTransportDoesNotImportTheDomain` in
`gitea/client_test.go`.
- **Update it when** a layering test is added, renamed, removed or weakened; when
a package is added or removed; or when an import that was forbidden becomes
allowed — that last one always comes with a reason, and the reason is what this
file is for.
- **Do not** restate what a package does. The table links to the file that says so.