Files
marketplace/cli/internal/mapping/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

6.7 KiB

AGENTS.md — internal/mapping (BRIDGE)

md ↔ Gitea's payloads. The whole translation, and only the translation.

Pure functions: no network, no filesystem, no flags, no clock. Give it a payload and it hands back a domain issue; give it an issue and it hands back a request body. That purity is the point — it can be reasoned about and tested without a Gitea anywhere, and it is the one package to open when the two representations disagree.

Imports issue, wire and the SDK. Nothing imports it but cmd — not the domain, not the transport. Both sides speak the SDK's shapes, which is what lets the two meet without either reaching into the other.

file what is in it
mapping.go the sync-owned metadata keys (gitea, url, synced, remote-updated, comments, branch), Origin, ApplyRemote
frompayload.go FromPayload and the accessors around it: LabelNames, AssigneeLogins, MergeCheckboxState
torequest.go the create/edit request bodies, LabelIDsFor
labels.go LabelColor, LabelSpecs, CanonicalLabelSpecs, DefaultColor
marker.go IDMarker, IDInBody, and the strip on the way in
layering_test.go the two tests that keep this package pure

What crosses the boundary, and what does not

domain          Gitea             note
----------------------------------------------------------------------
id (slug)       body marker       <!-- kettle:id … -->, first line of the
                                  tracker-side body; stripped out of the
                                  local copy — see marker.go
title           title             verbatim, both ways
body            body              verbatim up, verbatim down except the
                                  marker and checkbox state
state           state             open/closed, the same vocabulary
labels          labels[]          names both ways; ids only on write
assignees       assignees[]       logins
milestone       milestone.title   resolved to an id on write
depends         —                 slugs; #N is translated at this edge
—               number, html_url  lands in Extra as gitea:/url:
—               ref               Extra as branch:; push fills it from git

Only the login of a Gitea user crosses: it is what assignees: holds, and a display name is not an identity anything can be pushed against. Label and assignee lists are appended into a nil slice, so an issue with no labels is the same value as one loaded from a file — two spellings of "none" is a comparison bug waiting to happen.

depends: is the authoritative graph and is always slugs. The body's ## Depends on prose is passed through unchanged in both directions: a pull seeds depends: from the #N it finds there, and a push never rewrites what the author wrote. Deliberate — a translator that edits prose churns the body on every round trip.

The id marker

The one thing this package adds to a body, and it adds it because the slug has to survive a push: push deletes the local file, so the tracker has to be the thing that remembers what the issue was called here.

  • one place formats it (IDMarker), one regex reads it — and the regex accepts more than the formatter writes, including the older <!-- tea:id … -->, because issues pushed before the rename are still in the tracker;
  • the first valid marker wins; a second is ignored and removed on the way in;
  • the captured text must be a slug by the domain's own rule, so a mangled comment falls back to the title instead of naming a file after garbage;
  • it is stripped before anything else looks at the body, so checkboxes, #N references and what lands on disk all see the body the author wrote.

The checkbox merge

MergeCheckboxState is the one exception to "a pull overwrites the body", and deliberately the narrowest one that works. A tick is monotone — an item only travels [ ][x] — so the two sides are joined by a set union: no base version, no drift tracking, no conflict to resolve. An item comes out ticked when either side has it ticked; everything else in the body is still the remote's word.

Matching is on Checkbox.Text, which the domain parser has already stripped and rejoined with single spaces, so rewrapping a long item does not cost it its tick. It is otherwise literal: reword an item and it is a different item.

The same text more than once is read as the rule says, as a set — one ticked local item ticks every remote item with that text. Pairing duplicates up by order is the reading that can still drop a tick, and dropping a tick is the bug this exists to fix.

Labels, and the two write paths

Colours live here, not in the domain: a hex code is how a tracker paints a chip and not what an issue is. CanonicalLabelSpecs is derived from the domain's own list rather than restated, so adding a type over in the taxonomy creates it on the next bootstrap with no line changing here but the colour. DefaultColor paints everything outside the canonical set, because tech/* and comp/* are project-specific and guessing a colour for one invents a meaning it does not have.

LabelIDsFor is exported so that a create and a repair cannot derive the answer differently: Gitea's edit endpoint carries no labels, so an issue that already exists gets its label set through a PUT, and a PUT that disagreed with what a create would have sent would make a pushed issue and a re-pushed one two different things. nil means "resolved no ids"; an empty, non-nil list means "resolved some and matched none", which is a statement to the tracker — [] clears every label on the issue.

Purity, and the one weakening

layering_test.go checks direct imports and fails on os, net/http, os/exec, internal/gitea, internal/config and internal/project; a second test greps the sources for time.Now.

It does not walk the dependency closure, and it cannot: the SDK's types come with the SDK's client attached, so the graph contains an HTTP client whatever this package does with it. The full reasoning — and why time is allowed where it once was not — is in internal/AGENTS.md.

Keeping this file true

  • Scope: every .go file here — the field table, the marker, the merge, the colours.
  • Update it when a field starts or stops crossing the boundary (the table is the contract), a sync-owned metadata key is added, the marker spelling changes or an older one stops being read, or the purity test is loosened.
  • Do not put a request here that anything else could make. This package returns values; gitea sends them.