--- name: gitea-rest description: Use ONLY when an agent must call the Gitea REST API over HTTP (curl / fetch / HttpClient) — not for other gitea tooling (the retired gitea-mcp wrapper / gitea_* MCP tools). Covers auth schemes + token scopes, pagination, error format, this fork's specifics, copy-paste recipes for the daily loop (issues, PRs, comments, labels, releases, raw files), and the on-demand recipe for extracting any endpoint's definition from the auto-generated swagger spec (`/swagger.v1.json`) instead of keeping an endpoint catalog in context. This is the only supported Gitea API path — the gitea-mcp wrapper is retired. triggers: # Direct Gitea HTTP/REST. Composite "gitea rest"/"gitea api" to avoid # colliding with other gitea tooling. - gitea rest - gitea api - gitea http - call gitea api - /api/v1 - gitea swagger - swagger.v1.json - 调gitea api - gitea 接口 - gitea rest api role: Producer --- > Core 中立版(Increment 6a 改写)。本文件同时作为 gitea adapter 的 **reference implementation** 示例(frontmatter 原样);实例术语对照 `core/adapters/TERMINOLOGY.md`。 # Gitea REST — conventions + on-demand endpoint lookup Gitea ships a JSON REST API under **`/api/v1`**. The instance also publishes an **auto-generated OpenAPI spec at `/swagger.v1.json`** (~889KB, 316 endpoints) — never load it whole into context; extract single endpoint definitions with jq (see "Endpoint lookup"). The spec is generated from this fork's code, so it is always current — no manually maintained catalog can drift. ## Quick start ```sh BASE= TOKEN=$(cat /octopus/gitea-token) # or $WORKSPACE_GIT_TOKEN inside a workspace container AUTH="Authorization: token $TOKEN" curl -fsS -H "$AUTH" $BASE/api/v1/version # => {"version":"1.22.0"} ``` Inside a workspace container: `WORKSPACE_GIT_TOKEN` already carries the provisioned token, and the instance host resolves to the VPC-internal address (no EIP bandwidth cap) — use it as `TOKEN` directly. ## Authentication | Scheme | Header | Notes | | --------------------- | ------------------------------------------ | ----------------------------------------------- | | **PAT (recommended)** | `Authorization: token ` | Also accepts `bearer`. 40-char hex. | | OAuth2 JWT | `Authorization: bearer ` | OAuth2-app token. | | Basic | `Authorization: Basic ` | Server must enable it; 2FA needs `X-Gitea-OTP`. | - Query-param token (`?token=`) is deprecated — avoid. - `Sudo: ` header acts as another user (admin only). - Keep tokens out of git and out of process argv where possible: read from a file/env into a shell var at call time, not in a committed script. ## Token scopes (write implies read) Categories: `issue`, `repository`, `organization`, `user`, `notification`, `package`, `admin`, `misc`, `activitypub` — each as `read:X` / `write:X`. HTTP method sets the level: **GET → read**, **POST/PUT/PATCH/DELETE → write**. A scope miss returns `403` with `token does not have at least one of required scope(s)...` — re-issue the token rather than widening other permissions. The full agent loop (push commits, create/merge PRs, comment) needs at minimum **`write:repository` + `write:issue`**. Workspace container tokens are provisioned `read:repository` by default — write operations fail with 403 until the operator widens the scope. ## Pagination - `page` (1-based, default 1), `limit` (default 30, **hard cap 50**). - `X-Total-Count` response header = total results. - `Link: ; rel="next"` — walk `next` links until absent. - Some endpoints (commits, pulls) also set `X-Page` / `X-PerPage` / `X-PageCount` / `X-HasMore`. ## Errors Every error is `{"message":"...","url":".../api/swagger"}` with the matching status: `400` bad arg, `403` permission/scope, `404` not found or wrong BASE, `409` already exists, `413` too large, `422` malformed JSON / missing field. There is **no rate limiting** on `/api/v1` — don't expect `X-RateLimit-*`. ## Endpoint lookup (swagger, on demand) For any endpoint not covered by the recipes below, pull the spec once per session and jq out just the definition you need (50–200 tokens each): ```sh curl -s $BASE/swagger.v1.json -o /tmp/gitea-sw.json jq '.paths["/repos/{owner}/{repo}/pulls"].post' /tmp/gitea-sw.json # create PR jq '.paths["/repos/{owner}/{repo}/issues"].get.parameters' /tmp/gitea-sw.json jq '.definitions.CreateIssueOption' /tmp/gitea-sw.json # a body model jq -r '.paths | keys[]' /tmp/gitea-sw.json | grep actions # discover endpoints ``` Definitions include parameter names/types/required flags and `$ref` response models (under `.definitions` on this Gitea version). Prefer discovering via the spec over guessing paths. ## Daily-loop recipes `BASE`/`TOKEN`/`AUTH` as in Quick start; `OWNER=Octopus REPO=octopus` as the example. All bodies are JSON. **Create an issue** ```sh curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/issues" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"title":"Bug: X fails","body":"steps...","labels":[12]}' ``` **Comment on an issue / PR** (same path for both — PRs are issues by index) ```sh curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/issues/42/comments" -H "$AUTH" \ -H 'Content-Type: application/json' -d '{"body":"LGTM"}' ``` **Create a PR** ```sh curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"head":"workflow/fix/x","base":"main","title":"fix: X"}' ``` **Merge a PR** ```sh curl -fsS -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls/7/merge" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"Do":"merge","MergeTitleField":"feat: add X","MergeMessageField":"closes #7"}' ``` **Merge returned 405? Read the response body — two distinct causes:** - `{"message":"head branch is behind the base branch"}` — the keep-mergeable bot advanced main after your push, or Gitea's mergeable cache is stale. Fix: `git fetch origin main && git merge origin/main && git push` the head branch, wait ~2s, retry the merge. (Recurring on this server — seen 3 PRs in a row on 2026-08-23.) - `{"message":"The PR is already merged"}` — **treat as success**. The merge landed during a previous "failed" attempt (racy async recompute). Confirm with `GET .../pulls/7 | jq '.merged, .merge_commit_sha'` before assuming failure. Never re-push/re-create the PR on this signal. **Auth hygiene** — always pass the token via the `Authorization` header (`-H "Authorization: token $TOKEN"`), never embedded in a remote URL (`https://user:token@host/...`): it persists into `.git/config` and shell history. If a token leaks into a remote URL, rewrite the remote (`git remote set-url`) and rotate. **List PRs / changed files** — `GET .../pulls?state=open`, `GET .../pulls/7/files` **Search issues across repos** — `GET $BASE/api/v1/repos/issues/search?q=&type=pulls` **List labels** — `GET .../labels?limit=50 | jq '.[] | {name,color}'` (label create/update via POST/PATCH on the same path) **Raw file contents** — `GET .../raw/README.md` (plain text; the `contents/{path}` variant returns base64 in `.content`) **Create a release** — `POST .../releases {"tag_name":"v1.2.0","target":"main",...}` **Create a wiki page** — `POST .../wiki/new {"title":"My Page","content_base64":"","message":"add page"}`. **`content_base64` is the ONLY field that writes body text** — this fork **silently ignores** the upstream-style `content` field on wiki create/update (no error, `201` returned, page saved 0 bytes; evidence [org-internal #3944]: commits f5eaf18/4f0217d/bad5880). `content` is valid only on the file API, never on wiki. **Read / edit / delete a wiki page** — `GET|PATCH|DELETE .../wiki/page/{pageName}`. **Read by the mangled name, not the logical title**: this fork rewrites stored filenames for titles containing `/`, spaces, or `:` (slash percent-encoded into the filename + a `.-` suffix — see [org-internal #3218]), so `GET /wiki/page/bugfix-3204/bugfix-report` 404s. Always `GET .../wiki/pages` first and use the returned `sub_url` **verbatim** (it already carries the `%2F` encoding and `.-` suffix). PATCH/DELETE take the same mangled `pageName`. For PATCH, **omit `title` to keep the page name** and send only `content_base64`+`message` (fixed in gitea `dev-421-g7ff56aec08`, [org-internal #3510] — before that a title-less PATCH silently renamed the page to `unnamed.md` and later edits deleted target pages). Same trap as create: a PATCH carrying `content` instead of `content_base64` returns `200` with a **0-byte page** ([org-internal #3944]) — always base64-encode the body and verify non-empty via a follow-up `GET`. ## This fork's specifics - **Projects are repo-level only** — no `/orgs/.../projects` or `/users/.../projects` REST endpoints. - **Aggregated inline review comments**: no single list-all endpoint — list reviews, then fetch each review's `/comments`. - Wiki REST = 6 method×path combos on 4 paths (`new`, `page/{pageName}` GET/PATCH/DELETE, `pages`, `revisions/{pageName}`). This fork has **no** upstream `raw/{pageName}` or `pages/{pageName}` single-page endpoints, and `page/{pageName}` only matches the mangled filename (see the wiki recipe above and [org-internal #3218]) — clone the wiki git repo (default branch `main`) only if you need history beyond `/wiki/revisions` or bulk filename surgery. - The authoritative route table is `routers/api/v1/api.go` in the Gitea source; the human-readable docs UI is `/api/swagger`. ## In Effect code Prefer `HttpClient` from `@effect/platform` with the same headers (see `rules/effect-rules` on the wiki). Outside Effect, `curl` / `Bun.fetch` are fine — the wire format is identical.