# GitLab Integration — Implementation Plan **Spec:** `./spec.md` **Issue:** [#820](https://github.com/kdlbs/kandev/issues/820) ## 1. Architecture decisions **One package, mirror don't merge.** A new `internal/gitlab/` package mirrors the layout of `internal/github/`. Do **not** introduce a unifying `codehost.Client` interface in v1. The GitHub package is ~14k LOC and encodes deep GitHub-specific knowledge (search query DSL, review states, mergeable states, action presets). Premature abstraction would hurt both packages. Defer the unifying interface to v2 — once the GitLab package is real, the shared shape is concrete instead of guessed. **Two clients, both shipped in v1.** Implement `pat_client.go` against GitLab REST API v4 (Go HTTP client, similar shape to `internal/github/pat_client.go`) **and** `glab_client.go` that shells out to the `glab` CLI (similar shape to `internal/github/gh_client.go`). `factory.go` selects: mock env var → `glab` CLI if installed and authenticated → PAT (env var, then secrets store) → noop. Same selection order as the GitHub package so users with `glab` already configured pick up zero-config, and PAT remains the path for headless / corp-managed environments. **Auth: token-based, per-workspace host.** Token in `secrets.SecretStore` under name `GITLAB_TOKEN` (env var `GITLAB_TOKEN` also accepted). Self-managed host URL stored as a per-workspace setting (`gitlab_host`, default `https://gitlab.com`). Per-workspace because different workspaces commonly target different GitLab instances (work vs personal). **Data model: parallel tables, shared field semantics.** New tables `gitlab_mr_watches`, `gitlab_review_watches`, `gitlab_issue_watches`, `gitlab_action_presets` parallel to their `github_*` counterparts. The existing `Repository.Provider` field already supports `"gitlab"`; no schema change there. PR/MR data is **not** unified — they're sibling concepts with different fields (e.g. GitLab pipelines vs GitHub checks, GitLab approvals vs GitHub reviews). **`pr` skill: provider-aware, single skill.** Update `.agents/skills/pr/` to detect the repo provider from `git remote` and branch on host. For GitLab repos, use REST API directly via `curl`+`GITLAB_TOKEN` — no `glab` dependency in the agent container. ## 2. File map ### Backend — new package `internal/gitlab/` Mirrors `internal/github/` (~40 files) but deliberately smaller in v1. Files to create: - `models.go` — `MR`, `MRReview` (approvals), `MRComment`, `Pipeline`, `MRFeedback`, `MRStatus`, `MRSearchPage`, `Issue`, `IssueSearchPage`, `Project`, `RepoBranch`, `MRWatch`, `ReviewWatch`, `IssueWatch`. Shape parallel to `github/models.go` so frontend stores can share component families. - `client.go` — `Client` interface (parallel to GitHub's, but with GitLab nouns: `GetMR`, `FindMRByBranch`, `ListAuthoredMRs`, `ListReviewRequestedMRs`, `SubmitMRApproval`, `ListPipelines`, `GetMRDiscussions`, `ResolveDiscussion`, `CreateMRDiscussionNote`). - `pat_client.go` — REST v4 implementation. Per-instance host URL. Uses `PRIVATE-TOKEN` header for auth. Methods needed: - Projects: `/projects?membership=true`, `/projects/:id`, `/projects/:id/repository/branches`. - MRs: `/projects/:id/merge_requests`, `/merge_requests` (search), `/projects/:id/merge_requests/:iid`, `/projects/:id/merge_requests/:iid/discussions` (read + post + resolve), `/projects/:id/merge_requests/:iid/approvals`, `/projects/:id/merge_requests/:iid/changes`, `/projects/:id/merge_requests/:iid/commits`. - Pipelines: `/projects/:id/pipelines`, `/projects/:id/pipelines/:pipeline_id/jobs`. - Issues: `/issues` (search), `/projects/:id/issues/:iid`. - User: `/user` (auth check + login). - `pat_client_test.go` — table-driven tests with `httptest.Server` per endpoint group, mirroring `github/pat_client_test.go`. - `glab_client.go` — shells out to `glab` CLI for the same `Client` interface. Mirrors `internal/github/gh_client.go`: `GLabAvailable()` detects the binary, `IsAuthenticated()` runs `glab auth status`, and each method composes a `glab api …` or `glab mr …` invocation, parsing JSON stdout. Where `glab` lacks a direct command (e.g. specific search filters), fall through to `glab api` against the REST endpoint. - `glab_client_test.go` — uses `exec.LookPath` shimming and a fake `glab` binary on `PATH` (same pattern as `gh_client_test.go`). - `mock_client.go` + `mock_client_test.go` + `mock_controller.go` — E2E mock surface, gated by `KANDEV_MOCK_GITLAB=true`. Uses the `internal/integrations` shared shapes? **No** — copy the GitHub pattern (the mock currently lives outside `internal/integrations`). Match whichever pattern the team has standardized on (Linear/Jira use `internal/integrations`; GitHub does not). For consistency with GitHub-the-code-host, follow the GitHub pattern. - `noop_client.go` — null-object fallback when unauthenticated. - `factory.go` — `NewClient(ctx, secrets, host, log)`. Order: mock env var → `glab` CLI (if installed and authenticated for the configured host) → PAT (env var `GITLAB_TOKEN`, then secrets store key `GITLAB_TOKEN`) → noop. - `service.go` — coordinator parallel to `github.Service`. Accepts the per-workspace host. Methods: status, MR list/search, MR feedback (incl. discussions), watches CRUD, action presets CRUD, sync. **Use `internal/integrations/healthpoll`** for the auth-health loop — that's the shared utility Jira/Linear already use, and the GitHub package predates it. New code should pick the better pattern. - `service_*_test.go` — tests for issue, status, sync, watches, search cache (parallel to GitHub's split). - `service_task_events.go` — subscribes to task events and resyncs when MRs are linked. Mirror GitHub. - `store.go` — SQLx persistence for the four new tables + watches/presets read/write. Migrations next. - `store_test.go` — multi-project / multi-workspace round-trip. - `controller.go` — HTTP routes under `/api/v1/gitlab/*`: `GET /status`, `GET/POST/PUT/DELETE /mr-watches`, `GET/POST/PUT/DELETE /review-watches`, `GET/POST/PUT/DELETE /issue-watches`, `GET /mrs/:projectId/:iid/feedback`, etc. - `handlers.go` — WS dispatcher registrations (`ActionGitLabStatus`, `ActionGitLabMRList`, …). - `poller.go` — review-watch + issue-watch background loop. Reuse `healthpoll.Poller` for auth health; keep MR/issue polling in this package (different cadence, payload-shaped). - `connectivity.go`, `constants.go`, `ttl_cache.go` — verbatim parallels. - `provider.go` — `Provide(writer, reader *sqlx.DB, secrets SecretStore, eventBus bus.EventBus, log *logger.Logger) (*Service, func() error, error)`. ### Backend — schema migrations - `apps/backend/internal/task/repository/migrations/NNNN_gitlab_tables.sql` — create `gitlab_mr_watches`, `gitlab_review_watches`, `gitlab_issue_watches`, `gitlab_action_presets`. Use `gitlab_*` prefix on every column to avoid collisions when the unifying refactor lands. - `apps/backend/internal/workspace/...` — add `gitlab_host` column to the workspace settings table (or, if workspace settings are JSON, add the field to the JSON shape). Default `https://gitlab.com`. ### Backend — wiring - `cmd/kandev/services.go` — new `initGitLabService(...)` helper that resolves the per-workspace host (or a global default for unscoped callers) and wires the service. - `cmd/kandev/helpers.go` — register routes via `gitlab.RegisterRoutes` and `gitlab.RegisterMockRoutes`. - `internal/repoclone/protocol.go` — replace the `gitlab.com` hardcode with a host parameter. `CloneURL` signature gains `host string`. Update call sites in `internal/agent/lifecycle/executor_*.go` and `internal/orchestrator/executor/executor_execute.go` to pass the workspace's `gitlab_host` when `provider == "gitlab"`. - `internal/agent/credentials/env_provider.go` — already lists `GITLAB_TOKEN` in `knownAPIKeyPatterns`. No change required. - `internal/agent/lifecycle/default_scripts.go` — extend the default prepare script so HTTPS clones for GitLab use `https://oauth2:${GITLAB_TOKEN}@//.git`. Existing GitHub equivalent is the model. ### Frontend — new pages and components - `apps/web/app/gitlab/page.tsx` + `gitlab-page-client.tsx` — mirror of `app/github/`. SSR fetches `/api/v1/gitlab/status`, hydrates store. - `apps/web/app/settings/integrations/gitlab/page.tsx` — mirror of `settings/integrations/github/page.tsx`. Hosts the `GitLabIntegrationPage` panel: connection status banner, host URL field, PAT field, reconnect CTA. - `apps/web/components/gitlab/` — domain components. The MyGitHub component family (`my-github/pr-list`, `my-github/issue-list`, presets sidebar, search bar, action presets, save-preset dialog, pagination, list toolbar, quick task launcher) is large and tightly coupled to the GitHub data shape. Two viable approaches: - **(A) Extract shared components.** Move generic pieces (search bar, pagination, save-preset dialog, list toolbar, presets sidebar skeleton) into `components/code-host/` parameterised on item shape. Wire both `github` and `gitlab` to them. Higher upfront cost, no duplication. - **(B) Copy-and-adapt.** Duplicate the components folder, swap PR types for MR types. Lower upfront cost, two parallel codebases to maintain. - **Recommendation: (A) for shells, (B) for body.** Extract the container components (sidebar, toolbar, pagination, save dialog) — they're already provider-agnostic in shape. Copy the row renderers (`pr-row.tsx` → `mr-row.tsx`) where the visible fields differ (approvals vs reviews, pipelines vs checks). - `apps/web/lib/state/slices/gitlab/` — new slice mirroring `slices/github/`. Same hydration patterns. - `apps/web/hooks/domains/gitlab/` — `use-gitlab-status`, watches, search, action presets. Symmetric to `hooks/domains/github/`. - `apps/web/lib/api/domains/gitlab-api.ts` — REST client. - `apps/web/lib/types/gitlab.ts` — `GitLabMR`, `GitLabIssue`, etc. ### Frontend — kanban top-bar - `apps/web/components/kanban/kanban-header.tsx` — add `GitLabTopbarButton` next to `GitHubTopbarButton`. Render only when GitLab is configured for the active workspace (gate on `useGitLabAvailable`). - `apps/web/components/kanban/kanban-header-mobile.tsx` — same. ### Agent skill — `pr` - `.agents/skills/pr/SKILL.md` — add a "GitLab repos" section explaining the detection (`git remote get-url origin` matches `:gitlab|/gitlab`) and the GitLab MR-creation flow. Prefer `glab` CLI if available in the agent container; fall back to REST API via `curl` with `GITLAB_TOKEN`. Keep `gh` as the GitHub path. - `.claude/skills/pr/SKILL.md` symlink — already symlinked, no change. ### WebSocket actions - `pkg/websocket/actions.go` (or wherever GitHub actions are declared) — add `ActionGitLabStatus`, `ActionGitLabMRList`, `ActionGitLabMRGet`, `ActionGitLabMRFeedbackGet`, watches (review/issue/MR), pipeline status, action presets, stats. ## 3. Phases Each phase ends with `make fmt` then `make typecheck test lint` (backend) and `pnpm typecheck test lint` (web). ### Phase 1 — Backend client + auth (no UI) Goal: the backend can list one user's MRs over REST. - `internal/gitlab/{models,client,pat_client,glab_client,noop_client, factory,connectivity,constants}.go` plus tests. - Mock client + controller. Wired under `KANDEV_MOCK_GITLAB=true`. - Provider + `cmd/kandev/services.go` `initGitLabService`. - Status WS handler + `/api/v1/gitlab/status` HTTP route. - Migration for the new tables (kept empty-but-present so future phases can write). - Tests: PAT client per-endpoint, noop client, factory selection, controller status route. ### Phase 2 — Repo cloning + agent env Goal: a GitLab repo can be cloned and the agent has the token. - `repoclone/protocol.go` host parameter. Call-site updates. - `lifecycle/default_scripts.go` HTTPS-with-token rewrite for GitLab. - Workspace `gitlab_host` setting (read/write API + UI field deferred to Phase 3). - Tests: protocol unit test for SSH/HTTPS variants on custom host. Lifecycle prepare-script test for token injection. ### Phase 3 — Settings page + connection UX Goal: a user can connect/reconnect a GitLab account from the UI. - `app/settings/integrations/gitlab/page.tsx` + panel. - API endpoints: get/set host, set/clear token. - Reuse `` and `` from `internal/integrations` if those shells fit (they were built for Linear/Jira; check shape compat first). - Tests: settings panel render + token-save flow (vitest). ### Phase 4 — MR list page + watches Goal: full feature parity with the GitHub page for browse + watch. - Extract shared shells from GitHub MyGitHub components into `components/code-host/`. - New `components/gitlab/my-gitlab/` — row renderers, search-bar preset definitions, action-presets resolution. - `app/gitlab/page.tsx` + client. - Watch CRUD WS handlers + UI. - `gitlab` slice + hooks. - Poller for review-watches and issue-watches (uses `healthpoll.Poller` for auth-health; MR/issue polling in `gitlab/poller.go`). - Tests: store slice, hooks, save-preset, search. ### Phase 5 — MR review surface (#820 core) Goal: the agent UI shows MR feedback (discussions, approvals, pipeline) and the agent can post/resolve discussions. - `GetMRDiscussions` + `CreateMRDiscussionNote` + `ResolveDiscussion` in `pat_client.go`. - `MRFeedback` payload + `WS ActionGitLabMRFeedbackGet`. - Frontend: in the existing PR-feedback panel that lives next to a task, branch on `provider`. Reuse the comments/threads UI; map GitLab discussions onto the same shape (discussion = thread, notes = comments, `resolved` flag). - Tests: pat_client discussions round-trip, frontend feedback panel with a GitLab fixture. ### Phase 6 — `pr` skill update + #820 wiring Goal: clicking "Create PR" on a GitLab task opens a real MR. - `.agents/skills/pr/SKILL.md` — GitLab branch. - The kanban "Create PR" button already runs the `pr` skill on the task; no new wiring needed. Verify the surfaced MR URL flows back through the existing PR-watch path (Phase 4 watches consume it). - Manual smoke: open a kandev workspace pointing at a GitLab repo, edit, click Create PR, watch the MR appear, comment from gitlab.com, watch the agent see and resolve the comment. ### Phase 7 — Cleanup + docs - Remove `// TODO: GitLab support is not yet implemented` from `repoclone/protocol.go`. - Update `CLAUDE.md` "Repo Layout" and the integrations section to list `internal/gitlab/`. - Add the new spec/plan to `docs/specs/INDEX.md` (already done) and flip status to `shipped` once merged. ### Phase 8 — MR auto-link (post-ship follow-on) Goal: close the gap where an MR opened outside Kandev's own MR-creation action (`glab mr create`, or opening the MR directly in the GitLab web UI) never got linked — only manual URL linking and the Create-PR callback did. Mirrors GitHub's push-detection auto-link. - `internal/gitlab/service_task_mr_autolink.go` — `Service.AutoLinkMRForBranch` (find-MR-by-branch → validate repository identity → upsert association → ensure refresh watch → publish `gitlab.task_mr.updated`). - `internal/gitlab/service_watches.go` — `EnsureMRWatch` (idempotent get-or-create) and `CheckMRWatch` now refresh `gitlab_task_mrs` on every poll, not just watch timestamps. - `gitlab_mr_watches` unique key widened to `(session_id, repository_id, branch)` via an idempotent table-rebuild migration, so multi-branch tasks don't collide on the second branch's watch insert. - `internal/orchestrator/event_handlers_gitlab_mr.go` — `detectPushAndAssociateMR` (GitLab twin of GitHub's `detectPushAndAssociatePR`, same `[0, 30s, 60s]` retry shape) and `CheckSessionMR` (on-demand check, mirrors `CheckSessionPR`). `event_handlers_git.go`'s push-tracking dispatches by repository provider so the two providers' code paths never call into each other's client. `Status: shipped.` Out of scope for this phase (unchanged from the root spec): webhook-driven linking, and backfilling associations for MRs opened before this shipped. ## 4. Risks / open points - **API shape mismatches.** GitLab's MR discussions are nested (discussion → notes → resolved); GitHub's review comments are flat with `in_reply_to`. Mapping into the existing comments/threads store needs care — discussions are the natural unit on GitLab, not individual comments. Mitigation: GitLab discussions become first-class in the store, the GitHub side is mapped as "discussions of one note". Defer if it explodes scope; v1 can render two distinct components. - **Action-preset query syntax.** GitHub presets use the GitHub search DSL (`is:pr is:open author:@me`). GitLab MR search uses query parameters (`?author_username=…&state=opened`). The presets feature is GitHub-shaped; v1 ships GitLab presets as a constrained set (assignee/author/reviewer × open/merged) without full DSL parity. - **Self-managed TLS.** Self-hosted GitLabs sometimes use private CAs. `pat_client.go` uses the default `http.Client`. A workspace setting `gitlab_insecure_skip_verify` is the lazy answer; the right answer is reading `SSL_CERT_FILE`. v1 reads `SSL_CERT_FILE`; flag the self-signed escape hatch as a follow-up. - **Per-workspace vs global host.** Per-workspace adds per-call resolution overhead and complicates non-workspace-scoped callers (review/issue pollers). Mitigation: the poller iterates workspaces and uses each workspace's host; the MyGitLab page is workspace-scoped by construction. - **`glab` CLI parity.** `glab` lacks direct commands for some search filters that REST exposes; the CLI client falls back to `glab api` for those, which means the `glab` binary version matters less than the underlying REST endpoint version. Pin a minimum `glab` version in `GLabAvailable()` and surface the version mismatch in the status WS payload so the settings page can warn. - **Frontend component extraction (Phase 4).** Risk of refactor churn on GitHub during the extraction. Mitigation: extract one-component-at-a-time behind matching tests; revert the extraction if scope creeps and just copy-and-adapt instead. - **Mock harness scope.** GitHub's mock has 14k LOC of behavior to emulate; GitLab's mock can ship with a thin slice (status, MR list, MR feedback, watch round-trips). The Playwright e2e suite is the forcing function — match what tests need, not full parity. ## 5. Out of scope (per spec) Webhook ingestion, GitLab CI editing, GitLab-Issues-as-tracker, group-wide dashboards, OAuth login, Bitbucket. Two natural follow-ups once shipped: webhook ingestion (kills the pollers) and the unifying `codehost.Client` interface (now that two real clients exist).