feat: reach the rest of Gitea with kettle api, and drop tea
The plugin required `tea`, Gitea's own CLI, for everything that is not an issue: releases, pull requests, milestones, branches, actions, webhooks. That put a second binary, a second set of logins nothing here could see, and 400 lines documenting somebody else's flags outside anything this repository can test. One command over the transport that already existed removes all three. Transport: `post` — the hand-rolled request the SDK cannot express, written for the dependency endpoint — is generalized to an exported `Do`, and `post` is three lines on top of it. Same http.Client, so the same RoundTripper files the body under .kettle/payload/, the same `token …` header authenticates it, and a non-2xx is the same *APIError. It does not paginate, does not reformat the answer, and names no domain concept, so the layering test is untouched. The endpoint rule is `tea api`'s, so an endpoint table written for that tool still works — with one restriction it did not have: a full URL must be on this instance. Every request carries the project's token in a header, and a URL on another host would hand the token to whatever was typed. Command: `kettle api <endpoint>` in a new `api` group, so the generator writes plugins/kettle/skills/api/SKILL.md — group, directory and /kettle:api are one word. No --repo and no --login, for the reason no sync command has them: a cross-repository address is an address, and another instance is KETTLE_URL. `-X DELETE` needs `--yes`; a flag typed on purpose is an operator's decision. Scopes: a token minted for issues carries write:issue and answers 403 on the first request outside issues, naming no scope. Gitea cannot be asked what a token may do — its own token listing needs a password — so `auth add --scopes` records it, `auth list` and `config` show it, and a 403 says which category it is likely to be. Documentation only; nothing is checked against it. skills/use — the tea reference, 239 lines of it — becomes skills/api: what to ask for, which endpoints paginate, and how to write a body. Every mention of `tea` as a requirement is gone from the manifests, the READMEs, the runner and the four other skills; what survives is the back-compat with the old plugin, which is a decision and not a debt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: api
|
||||
description: Everything Gitea has that is not an issue — pull requests, releases, tags, branches, milestones, labels, commits, actions, webhooks, notifications, tracked times, users, repositories — reached with `kettle api`, one endpoint per request, under the login the project already pins. Load when the user asks to open or review a PR, cut or edit a release, make a milestone or a tag, look at branches or commits, read notifications or actions, or hit any Gitea endpoint by hand. Issues are NOT handled here: /kettle:issue works on them offline and /kettle:sync moves them to and from the tracker.
|
||||
---
|
||||
|
||||
# /kettle:api — Gitea beyond issues
|
||||
|
||||
`kettle api <endpoint>` is one authenticated request to the Gitea this project is
|
||||
pinned to. No second tool, no second login: the URL, the token and the repository
|
||||
are the ones `/kettle:auth` and `kettle init` already resolved, and the request
|
||||
body is filed under `.kettle/payload/` like every other request kettle makes.
|
||||
|
||||
This skill is the map of what to ask for. `kettle help api` is the flag
|
||||
reference, and it is generated from the binary — the block at the bottom of this
|
||||
file is the same text.
|
||||
|
||||
## Issues are somewhere else
|
||||
|
||||
`kettle api` can reach an issue and must not be used to. An issue read this way
|
||||
comes back as a full JSON payload — every label object, every URL, the whole
|
||||
comment thread — which is precisely what the other two skills exist to keep out
|
||||
of the context window.
|
||||
|
||||
| Skill | Scope |
|
||||
|---|---|
|
||||
| `/kettle:issue` | issues as units of work — create, read, grep, validate, tick, dependency graph. Offline. |
|
||||
| `/kettle:sync` | moving issues between the local store and the tracker — pull, push, comment, close, evict. |
|
||||
|
||||
The one exception is an issue endpoint that is not about the issue's content:
|
||||
`issues/{n}/comments` is also **a pull request's** comment thread, and
|
||||
`PATCH issues/{n}` is also how a pull request's title and body are edited. Gitea
|
||||
numbers issues and pull requests in one sequence and serves both under
|
||||
`/issues/`.
|
||||
|
||||
## What is a command and what is a request
|
||||
|
||||
Reach for the command where there is one: it knows the format, the store and the
|
||||
round trip. Everything else is an endpoint.
|
||||
|
||||
| Subject | How to reach it | Pages? |
|
||||
|---|---|---|
|
||||
| issues (create, read, tick, validate) | `/kettle:issue` — offline, no request at all | — |
|
||||
| issues (pull, push, comment, close, evict) | `/kettle:sync` | handled |
|
||||
| the canonical `type/*` and `severity/*` labels | `kettle labels` | handled |
|
||||
| this repository's own releases, with binaries | `cd cli && make release TAG=v1.2.3` | — |
|
||||
| everything below | `kettle api` | see the column |
|
||||
|
||||
| Entity | Endpoint | Pages? |
|
||||
|---|---|---|
|
||||
| pull requests | `repos/{owner}/{repo}/pulls` | **yes** |
|
||||
| one pull request | `repos/{owner}/{repo}/pulls/{n}` | no |
|
||||
| create a pull request | `POST repos/{owner}/{repo}/pulls` | no |
|
||||
| edit a PR's title or body | `PATCH repos/{owner}/{repo}/issues/{n}` | no |
|
||||
| a PR's or issue's comments | `repos/{owner}/{repo}/issues/{n}/comments` | **yes** |
|
||||
| edit one comment | `PATCH repos/{owner}/{repo}/issues/comments/{id}` | no |
|
||||
| reviews on a PR | `repos/{owner}/{repo}/pulls/{n}/reviews` | **yes** |
|
||||
| merge a PR | `POST repos/{owner}/{repo}/pulls/{n}/merge` | no |
|
||||
| releases | `repos/{owner}/{repo}/releases` | **yes** |
|
||||
| one release by tag | `repos/{owner}/{repo}/releases/tags/{tag}` | no |
|
||||
| tags | `repos/{owner}/{repo}/tags` | **yes** |
|
||||
| branches | `repos/{owner}/{repo}/branches` | **yes** |
|
||||
| commits | `repos/{owner}/{repo}/commits` | **yes** |
|
||||
| milestones | `repos/{owner}/{repo}/milestones` | **yes** |
|
||||
| labels (all of them, not just canonical) | `repos/{owner}/{repo}/labels` | **yes** |
|
||||
| webhooks | `repos/{owner}/{repo}/hooks` | **yes** |
|
||||
| action tasks | `repos/{owner}/{repo}/actions/tasks` | **yes** |
|
||||
| tracked times | `repos/{owner}/{repo}/times` | **yes** |
|
||||
| the repository itself | `repos/{owner}/{repo}` | no |
|
||||
| notifications | `notifications` | **yes** |
|
||||
| who this token is | `user` | no |
|
||||
| an organization's repositories | `orgs/{org}/repos` | **yes** |
|
||||
|
||||
`{owner}` and `{repo}` are filled in from the project's configuration. A path
|
||||
that names a repository in full is left alone — `repos/other-owner/other/releases`
|
||||
reads another repository on the same instance, which is why there is no `--repo`
|
||||
flag. Another **instance** is `KETTLE_URL` and `KETTLE_TOKEN`, not a flag.
|
||||
|
||||
What the instance actually serves is its own version's business; its API docs at
|
||||
`<instance-url>/api/swagger` are the authority when an endpoint answers 404.
|
||||
|
||||
## Pagination is yours
|
||||
|
||||
**One invocation is one HTTP request.** `kettle api` never follows a list to its
|
||||
end, because a passthrough that silently stitched pages together would report as
|
||||
one answer something that was several.
|
||||
|
||||
So for every row marked **yes** above:
|
||||
|
||||
```bash
|
||||
kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50' # first page, 50 rows
|
||||
kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50&page=2'
|
||||
```
|
||||
|
||||
- `limit` is capped by the instance (`MAX_RESPONSE_ITEMS`, 50 by default); the
|
||||
default page size is 30.
|
||||
- **A short page is the last one.** Ask for 50, count what came back: fewer than
|
||||
50 means there is no page 3. That is the same rule the binary's own listings
|
||||
use, and it needs no response headers.
|
||||
- Quote any endpoint holding `?` or `&`, or the shell takes it apart.
|
||||
- Walking many pages of anything into your own context is a mistake before it is
|
||||
a request. Narrow the query (`state=`, `since=`, `q=`), or pipe through `jq`
|
||||
and keep the two fields you needed.
|
||||
|
||||
## Writing a body
|
||||
|
||||
Two ways, and the choice is about the body, not the endpoint:
|
||||
|
||||
```bash
|
||||
# small and flat: every value is a string
|
||||
kettle api --field body=lgtm repos/{owner}/{repo}/issues/7/comments
|
||||
|
||||
# anything real — multi-line, markdown, booleans, numbers, nesting
|
||||
mkdir -p tmp/release
|
||||
cat > tmp/release/v0-2-0.json <<'EOF'
|
||||
{"tag_name": "v0.2.0", "name": "v0.2.0", "draft": false,
|
||||
"body": "## Changes\n\nMulti-line markdown with `code`."}
|
||||
EOF
|
||||
kettle api --data @tmp/release/v0-2-0.json repos/{owner}/{repo}/releases
|
||||
```
|
||||
|
||||
- A body implies `POST`; anything else is `-X PUT`, `-X PATCH`, `-X DELETE`.
|
||||
- Newlines inside a JSON string are `\n`. Composing from a file:
|
||||
`jq -Rs '{body: .}' < body.md > tmp/pull/x.json`.
|
||||
- `--field` values are **always strings**. A `draft: false` or a number is a
|
||||
`--data` body — guessing types is how a `tag_name` of `1.0` goes up as a
|
||||
number.
|
||||
- Keep `tmp/` gitignored and keep the file: a `PATCH` is usually the same body
|
||||
with one line changed. `kettle` files its own copy under `.kettle/payload/`
|
||||
automatically; that directory is the transport's and nothing hand-made goes in
|
||||
it.
|
||||
- Attachments are `multipart/form-data` and this command sends JSON. Upload
|
||||
release binaries with the release tooling (`make release`), or the web UI.
|
||||
|
||||
## Deleting
|
||||
|
||||
`-X DELETE` needs `--yes` in the same invocation, and the refusal happens before
|
||||
anything is sent:
|
||||
|
||||
```bash
|
||||
kettle api -X DELETE --yes repos/{owner}/{repo}/releases/12
|
||||
```
|
||||
|
||||
That flag is the whole gate. **Whether a thing should be deleted is the
|
||||
operator's call, not a step in a plan** — ask, do not assume, and never widen a
|
||||
deletion past what was named.
|
||||
|
||||
## When it says 403
|
||||
|
||||
Gitea scopes a token as `<read|write>:<category>`, and a token minted to file
|
||||
issues carries `write:issue` and nothing else. Releases, pull requests, branches,
|
||||
tags and actions are all `repository`, so that token answers **403 on the first
|
||||
`kettle api` outside issues** — and the 403 names no scope.
|
||||
|
||||
`kettle auth list` shows what each login on this machine recorded; `kettle config`
|
||||
shows what this project resolved. Nothing can be read back off the instance
|
||||
(Gitea's own token listing needs a password, not a token), so a scope that was
|
||||
never written down is a scope nobody knows. Minting a new token is the operator's
|
||||
job, in the web UI — `/kettle:auth` has the procedure.
|
||||
|
||||
## What is not an API call at all
|
||||
|
||||
| Want | Do |
|
||||
|---|---|
|
||||
| check out a PR branch, clone, push | `git`. This is git's job and always was |
|
||||
| who am I | `kettle api user` |
|
||||
| open something in a browser | nothing here; hand the user the URL |
|
||||
| add a login, list logins, ssh keys | `/kettle:auth`, and adding one is the operator's |
|
||||
| administer users or the instance | nothing here. Not an agent's work |
|
||||
|
||||
The canonical issue format lives in
|
||||
[`../issue/references/format.md`](../issue/references/format.md) — it describes
|
||||
local files, not requests.
|
||||
|
||||
<!-- kettle:gen -->
|
||||
**Generated from the kettle command registry by `kettle gen skills`.** Everything between the two markers is replaced on the next run — hand-written prose belongs outside them.
|
||||
|
||||
## `kettle api <endpoint>`
|
||||
|
||||
one request to this project's Gitea, for everything that is not an issue
|
||||
|
||||
Releases, pull requests, milestones, branches, tags, actions, webhooks,
|
||||
notifications: everything Gitea has that this binary has no command for. One
|
||||
invocation is ONE request — the credentials, the repository and the payload
|
||||
scratchpad are the ones this project already resolved, so there is nothing to
|
||||
configure and no second tool to log in.
|
||||
|
||||
THE ENDPOINT IS SPELLED THE WAY GITEA'S OWN DOCUMENTATION SPELLS IT. A bare path
|
||||
is taken as relative to `/api/v1/`; a path that already begins `/api/` is sent as it
|
||||
stands, which is how anything outside v1 is reached; a full URL is allowed only
|
||||
on the instance this project points at, because every request here carries the
|
||||
project's token in a header and a URL somewhere else would hand that token over.
|
||||
`{owner}` and `{repo}` are filled in from the project's configuration. Quote an
|
||||
endpoint that contains ? or & or the shell will take it apart.
|
||||
|
||||
ANOTHER REPOSITORY NEEDS NO FLAG — write its address into the path
|
||||
(`repos/other-owner/other-repo/releases`) and nothing is substituted. There is no
|
||||
--repo and no --login here for the same reason there is none on push or pull:
|
||||
which login a project runs under is a fact about the project. Another INSTANCE
|
||||
is KETTLE_URL and KETTLE_TOKEN, which is also what a CI run uses.
|
||||
|
||||
THE ANSWER IS THE SERVER'S BYTES ON STDOUT, unparsed and unreformatted — pipe it
|
||||
to jq, redirect it to a file. There is no flag that names an output file: in
|
||||
this tree --out is the issue store, and one word meaning two things is exactly
|
||||
the trap the tool this replaces set with an -o that wrote a file called "json".
|
||||
|
||||
IT DOES NOT PAGINATE. One call is one request, so a listing answers with one
|
||||
page: ask for the next with ?page=2, and for a bigger one with ?limit=50 (the
|
||||
server's own default is 30, its maximum is usually 50). A passthrough that
|
||||
stitched pages together silently would report as one answer something that was
|
||||
several.
|
||||
|
||||
ISSUES ARE NOT THIS COMMAND'S JOB even though it can reach them. An issue read
|
||||
this way arrives as a full JSON payload — every comment, every label object,
|
||||
every URL — which is what /kettle:issue and /kettle:sync exist to keep out of a
|
||||
context window. Use pull, push, comment and close.
|
||||
|
||||
A 403 here is usually the token rather than the request: a token minted for
|
||||
issues carries write:issue, and releases, pull requests, branches and tags are
|
||||
all under repository. `kettle auth list` shows what each login records.
|
||||
|
||||
-X DELETE NEEDS --yes. Everything else goes through as typed; a deletion does
|
||||
not, because a flag typed on purpose is an operator's decision and the URL of a
|
||||
release is one character away from the URL of the wrong release.
|
||||
|
||||
What it cannot do: an upload. Release attachments are multipart/form-data and
|
||||
this sends JSON — the release tooling in cmd/release does those.
|
||||
|
||||
| flag | default | what it does |
|
||||
| --- | --- | --- |
|
||||
| `--X` | — | the same flag as --method, spelled the way curl and the tool this replaces spell it |
|
||||
| `--data` | — | the request body: @file, @- for standard input, or the JSON itself |
|
||||
| `--field` | — | key=value, added to a JSON body as a string; repeatable |
|
||||
| `--method` | — | GET, POST, PUT, PATCH or DELETE (default GET, or POST when there is a body) |
|
||||
| `--status` | `false` | print the status line on standard error |
|
||||
| `--yes` | `false` | confirm a DELETE |
|
||||
|
||||
```bash
|
||||
kettle api repos/{owner}/{repo}/releases # the latest page of releases, as JSON
|
||||
kettle api user # who this project's token belongs to
|
||||
kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50' # quote anything with ? or & in it
|
||||
kettle api --data @tmp/release/v0-2-0.json repos/{owner}/{repo}/releases # a body from a file; POST is implied
|
||||
kettle api --field body=lgtm repos/{owner}/{repo}/issues/7/comments # a small body without a file
|
||||
kettle api -X DELETE --yes repos/{owner}/{repo}/releases/12 # a deletion, said out loud
|
||||
kettle api repos/{owner}/{repo}/milestones | jq '.[].title' # the bytes are the server's; jq is yours
|
||||
```
|
||||
<!-- /kettle:gen -->
|
||||
Reference in New Issue
Block a user