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,125 @@
|
||||
# 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`](../issue/AGENTS.md), [`wire`](../wire/AGENTS.md) and the SDK.
|
||||
Nothing imports it but [`cmd`](../cmd/AGENTS.md) — 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`](../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`](../gitea/AGENTS.md) sends them.
|
||||
Reference in New Issue
Block a user