8.7 KiB
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 | 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)
{ 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}; headerX-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 (serversincesupport not trusted).GET /repos/{o}/{r}/issues/{index}→ Issue withpull_requestnon-null when PR (same dual read as GitHub).GET|POST /repos/{o}/{r}/issues/{index}/comments(nosinceparam — 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/commentsMISSING 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)
deployment.ts: addGiteaSetupSchema = {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).app/setup.ts resolve(): if github config empty (no repository && no cli) → gitea path; else GitHub path unchanged. Gitea path: resolveGiteaDeployment;writeEnvironmentbranch when!github.cli && gitea: shims for blocked commands (message "act as on Gitea"), NO gh shim, NO github.com credential section;verifyEnvironmentskipped for gitea-only (credential check is github.com-specific).checkAccounts: blocksource:gitea→ resolveGiteaDeployment (place 'gitea' so unrelated errors don't block saves).src/setup.tsPROBLEMS: add gitea codes (place 'gitea'): giteaUrlUnset, giteaRepositoryUnset, giteaTokenMissing, giteaTokenInvalid, giteaRepositoryUnreadable, giteaRepositoryReadOnly.- 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). - Build wiring: tsdown entry
'gitea-watch': 'src/sources/gitea.ts'; package.json exports"./gitea-watch"; cordis.patch.yml bundle insertdsh-superagent-giteanamedsh-superagent/gitea-watchdisabled: 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).