113 lines
8.7 KiB
Markdown
113 lines
8.7 KiB
Markdown
# Gitea Source — design & decisions (working doc)
|
||
|
||
Goal per docs/008 Cookbook: new Source kind `gitea` (plugin `dsh-superagent-gitea`, files `src/sources/gitea*.ts`)
|
||
for the user's self-hosted **Gitea 1.25.4 at https://gitea.bitnasdaq.vip** (OpenAPI verified against the live
|
||
instance; dump at D:/Code/AI/gitea-spec.json). Gitea's REST API deliberately mirrors GitHub's shape
|
||
(`/notifications` + `X-Poll-Interval` header, Issue has `pull_request` meta, numbers shared Issue/PR sequence),
|
||
so `gitea.ts` mirrors `github.ts` structure with a direct HTTPS fetch client instead of the `gh` CLI.
|
||
|
||
## Domain map (cookbook step 1)
|
||
|
||
| Concept | Gitea mapping |
|
||
| --- | --- |
|
||
| Trigger | Issue body `key('gitea-issue', n)` (same format for PRs — shared number sequence); comment/review `key('gitea-comment', url||thread, id, updated_at)` |
|
||
| Thread | `String(number)` primary; full key `gitea:<number>` |
|
||
| Actor | `user.login` (allowed list names logins). Gitea 1.25.4 API has **no `is_bot` field** → `human: true` unless actor is the agent or on the Policy `bots` list (bots list is the lever). Muted/mirrored authors: `user` may be null → login `ghost`, id ''. |
|
||
| addressed | `mentions(body, login)` — same `@login` regex as github-trigger.ts |
|
||
| Channel address | `{ number, url }` — `url` = html_url of comment/issue, ALWAYS stored (cookbook: put every needed fact in the address) |
|
||
| Receiving scope rows | `issues` / `pulls` / `comments` (switches `observeIssues`/`observePulls`/`observeComments`, all default false). CI + inline review comments + PR-close binding = **M2, not MVP** |
|
||
|
||
## Config schema (stored in Settings.sources.gitea.config — generic record, zero core schema change)
|
||
|
||
```ts
|
||
{ url: '', // base, no trailing slash: ^https?://[^/]+$
|
||
repository: '', // owner/name
|
||
tokenEnv: 'GITEA_TOKEN', // NAME of env var holding the API token; token NEVER persisted (pattern: gh keeps creds out of config)
|
||
pollSeconds: 60, failuresBeforeError: 3,
|
||
observeIssues: false, observePulls: false, observeComments: false }
|
||
```
|
||
Token source: `process.env[tokenEnv]` in the **orchestrator host process** (DSH injects `~/.dsh/.credentials.yaml`
|
||
keys into process env → add `GITEA_TOKEN` there). Note: DSH strips credential-shaped env names from *worker*
|
||
children (infra/environment.ts header comment) — irrelevant, workers never need the token in MVP.
|
||
|
||
## Verified live endpoints (1.25.4)
|
||
|
||
- `GET /api/v1/notifications?all&since&limit&page` → `NotificationThread[]` {id, unread, repository{full_name},
|
||
subject{type,url,latest_comment_url,html_url,title,state}, updated_at}; header `X-Poll-Interval`.
|
||
- `GET /user` → {login,id}; `GET /repos/{o}/{r}` → {full_name, permissions{pull,push}}.
|
||
- `GET /repos/{o}/{r}/issues` (issues only), `/pulls` (PRs only) — unlike GitHub where /issues includes PRs!
|
||
Client-side filter updated_at >= cursor (server `since` support not trusted).
|
||
- `GET /repos/{o}/{r}/issues/{index}` → Issue with `pull_request` non-null when PR (same dual read as GitHub).
|
||
- `GET|POST /repos/{o}/{r}/issues/{index}/comments` (no `since` param — client filter on updated_at; pagination per_page=100&page=N until short page).
|
||
- `GET /repos/{o}/{r}/pulls/{index}/reviews` → PullReview[] {id,state,body,commit_id,user,html_url,submitted_at,updated_at}.
|
||
- **No repo-wide review-comment stream** (`/pulls/comments` MISSING in 1.25.4) → MVP review intake only via per-PR reads.
|
||
- CI for M2: `/repos/{o}/{r}/branches/{branch}` (user_can_push!), `/commits/{ref}/status` (CombinedStatus), `/actions/runs`.
|
||
|
||
## Poll flow (mirrors github.ts; MVP subset)
|
||
|
||
`pollRound` = 1) `pollNotifications` (KV gitea-since + per-number cursor KV `gitea-cursor:<n>` + retry map
|
||
`gitea-retry`; subject.url parsed by `giteaAddress` → read issue+comments[+reviews if PR]) →
|
||
2) `pollRepository` streams issues/pulls/comments (KV `gitea-stream:<id>`, cursorAt −5 s overlap, stable ids dedup,
|
||
horizon POLL_HORIZON_MS, `started` per kind) → 3) `pollThreads` subscribed threads via poll.ts `pollThreads`
|
||
(skip PR-close observation). 5 s test-timer-friendly `pause`. 3 consecutive round failures → health error
|
||
(`failuresBeforeError`). Own messages never Triggers. Health entry: `['gitea']`.
|
||
|
||
## Channel send (idempotent, GitHub pattern)
|
||
|
||
Marker `<!-- dsh-superagent:<message.id> -->` appended; before POST, list issue comments and if any comment by the
|
||
agent login contains the marker → 'sent'. POST `issues/{n}/comments` {body}. 'gone' when: GET issue → 404 (deleted) or
|
||
POST 403 `/locked/i`. Other throws → Outbox retries 5→300 s. `link()` = {url: address.url, label `Gitea #n`}.
|
||
`allowed(address, config)`: number parses && config.repository non-empty (address always has its own url).
|
||
|
||
## threadContext (assessments)
|
||
|
||
`messageId(trigger)`: issue body trigger → `${number}-body`; comment/review → `c${id}`/`r${id}`.
|
||
`read(part)`: fetch issue + all comments (page loop); messages = [body `${title}\n\n${body}`] + comments (+reviews
|
||
when PR and part allows), ContextMessage {id, from, at: updated_at??created_at, text, url}; 'root' → just body
|
||
message; flat forum — 'parent' returns the page containing the message (no threading in Gitea issue comments);
|
||
`next` = page cursor `p2` etc. body payload = the messages.
|
||
|
||
## Policy part
|
||
|
||
`allowed: [{id: login}]` regex `^[A-Za-z0-9._-]+$`; optional `bots` same shape (the substitute for missing is_bot —
|
||
documented hint: list automation accounts here). `explicit`: "名单里的账号在 Issue 或评论里 @ 智能体的账号。".
|
||
`rows`: issues/pulls/comments rows like github.ts (cells from observe* flags). No `sync` (self-hosted, no rosters).
|
||
|
||
## Environment variables (registered by this kind)
|
||
|
||
DSH_SUPERAGENT_GITEA_REPOSITORY (owner/name), DSH_SUPERAGENT_GITEA_URL (base url), DSH_SUPERAGENT_GITEA_ACCOUNT
|
||
(agent login — from deployment.gitea), DSH_SUPERAGENT_GITEA_NUMBER (issue/PR number that started ticket, else '').
|
||
No token vars in worker env ever.
|
||
|
||
## Core patch (required: `configured()` hard-requires gh — app/setup.ts resolve() → resolveDeployment throws)
|
||
|
||
1. `deployment.ts`: add `GiteaSetupSchema = {repository,url,tokenEnv}`, `SETUP_FIELDS.gitea`, `Deployment.gitea?:
|
||
{url, login}`; `resolveGiteaDeployment(machine, config, fetchImpl, env)`: url→GET /user (token env), repo→GET
|
||
/repos (canonical full_name + permissions.push required). Gitea-only Deployment shape:
|
||
`{ ...machine, repository: <gitea repo>, github: {cli:'',login:'',noreply:''}, gitea: {...} }` — github source
|
||
disabled in that profile, its readers never run (verified consumers: github.ts env/facts/poll only when enabled;
|
||
githubLink via registry per kind; writeEnvironment/verifyEnvironment patched next).
|
||
2. `app/setup.ts resolve()`: if github config empty (no repository && no cli) → gitea path; else GitHub path
|
||
unchanged. Gitea path: resolveGiteaDeployment; `writeEnvironment` branch when `!github.cli && gitea`: shims for
|
||
blocked commands (message "act as <gitea login> on Gitea"), NO gh shim, NO github.com credential section;
|
||
`verifyEnvironment` skipped for gitea-only (credential check is github.com-specific).
|
||
3. `checkAccounts`: block `source:gitea` → resolveGiteaDeployment (place 'gitea' so unrelated errors don't block saves).
|
||
4. `src/setup.ts` PROBLEMS: add gitea codes (place 'gitea'): giteaUrlUnset, giteaRepositoryUnset, giteaTokenMissing,
|
||
giteaTokenInvalid, giteaRepositoryUnreadable, giteaRepositoryReadOnly.
|
||
5. KV (`src/kv.ts`): `gitea: { since: record('gitea-since'), retries: record<Record<string,Retry>>('gitea-retry'),
|
||
cursor: family('gitea-cursor:'), stream: family('gitea-stream:') }` (tests/store.spec.ts requires key literals only there).
|
||
6. Build wiring: tsdown entry `'gitea-watch': 'src/sources/gitea.ts'`; package.json exports `"./gitea-watch"`;
|
||
cordis.patch.yml bundle insert `dsh-superagent-gitea` name `dsh-superagent/gitea-watch` **disabled: true**
|
||
(the router profile enables it; web profile must not). 007-architecture.md module+test tables (tests/docs.spec.ts
|
||
enforces completeness) + 001-domain "What is reusable" row. Tests: `tests/gitea.spec.ts` (fetch mocked via
|
||
injected fetchImpl / vi.stubGlobal), pattern of tests/sources.spec.ts.
|
||
|
||
## MVP cut lines (M2 later): Gitea PR observation + threadClosed/bindThread + auto-merge; inline review comments;
|
||
CI (branch + PR checks) triggers; agent-credential git helper for the Gitea host (MVP workers use the machine's
|
||
ambient git auth = user's own account for pushes; agent identity applies to API writes); prompt seeds that mention gh.
|
||
|
||
## Test plan order
|
||
translate → scope filtered → round (stored/filtered/cursor/dup) → mention explicit → send sent/gone/throw →
|
||
threadContext pages → apply registers + setupPending → policy rows → live smoke vs gitea.bitnasdaq.vip (needs user:
|
||
agent account + token + repo).
|