Files
dsh-superagent/GITEA-DESIGN.md
T

8.7 KiB
Raw Blame History

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}; 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 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).