forge CLI pivot: rewrite gitea+gitlab clients to tea/glab transports

Replace raw fetch transport with CLI subprocess calls:
- Gitea: spawn 'tea api --include' with GITEA_SERVER_TOKEN env var
- GitLab: spawn 'glab api --include' with GITLAB_TOKEN env var

Binary paths env-overridable (TEA_BIN / GLAB_BIN).
8s request timeout via AbortSignal on spawned process.
ETag cache and rate-limit cooldown dropped (tradeoff documented).
Pagination via --paginate for list endpoints.
Tests mock child_process.spawn instead of globalThis.fetch.
This commit is contained in:
2026-09-05 14:06:04 +00:00
parent aa0e3a222c
commit 77e7dd1127
6 changed files with 558 additions and 878 deletions
@@ -2,16 +2,16 @@
## Purpose
- This module owns GitLab auth (Personal Access Token), raw REST v4 client access, remote-URL repo resolution, and GitLab issue / merge-request (MR) APIs for OpenChamber, including MR create/update/merge writes.
- This module owns GitLab auth (Personal Access Token), CLI-backed REST v4 client access, remote-URL repo resolution, and GitLab issue / merge-request (MR) APIs for OpenChamber, including MR create/update/merge writes.
- From a user perspective, this is the layer that lets the app show GitLab issues and merge requests for a local project, including comments and per-file diffs, and create, edit, and merge merge requests.
- The module mirrors `packages/web/server/lib/github/` but uses a **Personal Access Token (PAT)** with a configurable base URL (gitlab.com by default, or a self-hosted instance), and talks to GitLab's REST v4 API directly via `fetch` — no new dependencies.
- The module mirrors `packages/web/server/lib/github/` but uses a **Personal Access Token (PAT)** with a configurable base URL (gitlab.com by default, or a self-hosted instance), and talks to GitLab's REST v4 API via the **`glab` CLI** transport instead of raw `fetch`.
## Entrypoints and structure
- `packages/web/server/lib/gitlab/index.js`: public server entrypoint re-exports.
- `packages/web/server/lib/gitlab/routes.js`: Express route registration for `/api/gitlab/*` endpoints.
- `packages/web/server/lib/gitlab/auth.js`: PAT auth storage, multi-account support, base URL normalization.
- `packages/web/server/lib/gitlab/client.js`: raw `fetch` GitLab REST v4 client (timeout, ETag conditional GET, rate-limit cooldown, pagination, redirect handling).
- `packages/web/server/lib/gitlab/client.js`: CLI-backed `glab api` client (process spawn with 8s timeout, `--include` for HTTP status/headers, `--paginate` for list endpoints). Token is passed via `GITLAB_TOKEN` env var.
- `packages/web/server/lib/gitlab/repo.js`: GitLab remote URL parsing and directory-to-repo resolution.
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: API route layer that calls this module (via `registerGitLabRoutes`).
- `packages/web/src/api/gitlab.ts`: web client wrapper for GitLab endpoints.
@@ -62,13 +62,12 @@ Nothing in the client or repo layers assumes the token came from a PAT.
## Client behavior
- Base URL joining: `{baseUrl}/api/v4{path}`. Project `:id` segments are URL-encoded with `encodeURIComponent` (e.g. `group/sub` -> `group%2Fsub`) and never double-encoded.
- Per-request timeout: 8000 ms via `AbortSignal.timeout`, unless the caller passes its own signal.
- ETag conditional-GET cache: keyed `token\nurl`, max 300 LRU entries; a `304` is replayed from cache as a `200`. GET only.
- Pagination: `x-page`, `x-next-page`, `x-total-pages`, and the `Link` header (`rel="next"`) are parsed into the returned `page` object (`hasMore` = a next page exists).
- Redirects: `301`/`302`/`308` with a `Location` header are followed exactly once (project moves) with `redirect: 'manual'`, preserving `PRIVATE-TOKEN` across the hop.
- Rate limits: a `429` records a module-level cooldown (honoring `Retry-After` / `RateLimit-Reset` when present) and surfaces `{ status: 429, error: 'GitLab rate limited' }`. While the cooldown is active, requests short-circuit without hitting the network.
- Transport: each call spawns a `glab api` process with `--include` for HTTP status/headers. Auth is passed via `GITLAB_TOKEN` env var (never on argv). Binary path: `GLAB_BIN` env or `/home/user/.local/bin/glab`.
- Base URL: the `baseUrl` parameter is passed to the client constructor for compatibility but `glab` resolves the instance from its own config. The `--include` flag provides HTTP status codes and response headers.
- Per-request timeout: 8000 ms via `AbortSignal.timeout` on the spawned process. The process is killed with `SIGKILL` on timeout.
- Pagination: `--paginate` is passed for GET requests with query params, causing `glab` to fetch all pages in a single call. The returned `page` object is `null` since pagination is handled by the CLI.
- `request` never throws for HTTP error statuses — callers branch on `status`.
- ETag cache and rate-limit cooldown have been dropped with the CLI pivot. Each call spawns a fresh process, so there is no persistent connection for conditional requests or shared rate-limit state. `isGitLabRateLimited()` always returns `false`; `noteGitLabRateLimit()` is a no-op.
## API integration overview
@@ -148,5 +147,5 @@ Conventions mirror `github/routes.js`:
- Keep the response shapes in lockstep with `GitLab*` types in `packages/ui/src/lib/api/types.ts`.
- Never log tokens. Error messages must not include the access token.
- Do not double-encode project paths; convenience methods already call `encodeURIComponent` on the `pathWithNamespace`.
- The ETag cache and rate-limit cooldown are module-level and per-instance — they are NOT shared with the GitHub module.
- The `glab` CLI handles authentication, base URL resolution, and pagination internally. The client does not maintain its own ETag cache or rate-limit cooldown — each call spawns a fresh process.
- To add further GitLab write operations, add the endpoint in `routes.js`, add a convenience method in `client.js`, and extend the shared types — mirror the existing issue/MR write routes and the GitHub PR write routes.