160 lines
7.3 KiB
Markdown
160 lines
7.3 KiB
Markdown
---
|
|
spec: docs/specs/integrations/requirements/gitlab-workflow-sync.md
|
|
created: 2026-08-06
|
|
updated: 2026-08-06
|
|
status: done
|
|
---
|
|
|
|
# GitLab Workflow Sync Plan
|
|
|
|
## Scope
|
|
|
|
Add a GitLab-backed source to workflow sync, alongside the existing GitHub one.
|
|
Backend: two new `gitlab.Client` methods, workspace-routed service wrappers, a
|
|
provider column plus `project_path` on the sync config, and provider dispatch in
|
|
`fetchFiles`. Frontend: a provider selector on the sync dialog and the matching
|
|
URL parse/build helpers. Everything downstream of fetching bytes is untouched.
|
|
|
|
## Current State
|
|
|
|
- `apps/backend/internal/workflowsync/service.go:32-39` — `ClientProvider` is
|
|
typed against `github.RepoContentEntry`; `github.Service` is the only
|
|
implementation.
|
|
- `apps/backend/internal/workflowsync/service.go:182-207` — `fetchFiles` calls
|
|
it directly and hardcodes a GitHub-specific unauthenticated error at line 184.
|
|
- `apps/backend/internal/workflowsync/store.go:31-66` — self-contained schema
|
|
with an idempotent `addPollEnabledColumn` migration precedent. No `provider`.
|
|
- `apps/backend/internal/workflowsync/models.go:38-110` — `Config`,
|
|
`SetConfigRequest`, and `Normalize()` assume GitHub owner/name.
|
|
- `apps/backend/internal/backendapp/services.go:571-578` — wires `githubSvc` as
|
|
the sole client provider.
|
|
- `apps/backend/internal/gitlab/client.go` — `Client` interface, ~30 methods,
|
|
all `projectPath`-first. **No repository tree or file-content method.**
|
|
- `apps/backend/internal/gitlab/service_config.go:432-458` —
|
|
`ClientForWorkspace` / `ClientForWorkspaceHost` already resolve a
|
|
per-workspace, revision-cached client.
|
|
- `apps/web/components/settings/workflow-sync-dialog.tsx` and
|
|
`apps/web/hooks/domains/settings/use-workflow-sync.ts` — GitHub-only, parse
|
|
via `@/lib/utils/github-repo-url`.
|
|
|
|
## Architecture
|
|
|
|
`Config.Provider` selects one of two injected provider clients. Each interface
|
|
keeps its own upstream listing shape (`github.RepoContentEntry` /
|
|
`gitlab.RepoTreeEntry`) at the boundary, and `workflowsync` converts both to a
|
|
provider-neutral `dirEntry` inside its fetch loop, so no provider-typed value
|
|
leaks past that conversion. Credential and host resolution stay inside the
|
|
respective integration packages — `workflowsync` never touches tokens.
|
|
|
|
```
|
|
Config.Provider ──┬── "github" ─→ GitHubClientProvider ─→ github.Service ─→ App/PAT client
|
|
└── "gitlab" ─→ GitLabClientProvider ─→ gitlab.Service.ClientForWorkspace ─→ PAT/glab client
|
|
│
|
|
└─→ []RepoEntry ─→ existing parse → apply → reconcile
|
|
```
|
|
|
|
## Backend Touch Points
|
|
|
|
| File | Change |
|
|
| --- | --- |
|
|
| `internal/gitlab/models.go` | `RepoTreeEntry` type. |
|
|
| `internal/gitlab/client.go` | `ListRepoTree`, `GetRepoFileContent` on the interface. |
|
|
| `internal/gitlab/pat_client.go` | REST impl: tree (paginated) + raw file. |
|
|
| `internal/gitlab/mock_client.go` | Seedable in-memory impl. |
|
|
| `internal/gitlab/noop_client.go` | Not-configured stubs. |
|
|
| `internal/gitlab/service_repo_contents.go` *(new)* | Workspace-routed wrappers via `ClientForWorkspace`. |
|
|
| `internal/workflowsync/service.go` | `RepoEntry`, two provider interfaces, dispatch in `fetchFiles`, provider-aware errors. |
|
|
| `internal/workflowsync/models.go` | `Provider`, `ProjectPath`, provider-conditional `Normalize()`. |
|
|
| `internal/workflowsync/store.go` | Two `ADD COLUMN` migrations; select/scan/upsert updates. |
|
|
| `internal/workflowsync/provider.go` | `Provide` accepts both providers. |
|
|
| `internal/backendapp/services.go` | Pass `gitlabSvc` into `initWorkflowSyncService`. |
|
|
|
|
## Frontend Touch Points
|
|
|
|
| File | Change |
|
|
| --- | --- |
|
|
| `lib/types/workflow-sync.ts` | `provider`, `project_path` on config + request types. |
|
|
| `lib/utils/gitlab-repo-url.ts` *(new or extend existing)* | Parse/build GitLab project URLs incl. subgroups and self-managed hosts. |
|
|
| `hooks/domains/settings/use-workflow-sync.ts` | Provider state; route parse/build by provider. |
|
|
| `components/settings/workflow-sync-dialog.tsx` | Provider selector mirroring `REMOTE_REPOSITORY_PROVIDERS`; provider-aware placeholder and labels. |
|
|
| `apps/web/locales/**` | New i18n keys; no hardcoded copy (root `CLAUDE.md` i18n ratchet). |
|
|
|
|
## Tests
|
|
|
|
Backend, colocated `*_test.go`:
|
|
|
|
- `gitlab`: tree pagination, URL-encoding of nested `project_path`, raw file
|
|
fetch, 404/403 mapping, mock and noop conformance to the interface.
|
|
- `workflowsync/models_test.go`: `Normalize()` matrix — provider default,
|
|
invalid provider, GitHub rejects `project_path`, GitLab rejects
|
|
owner/name, nested path accepted, `..`/empty-segment/leading-slash rejected.
|
|
- `workflowsync/store_test.go`: fresh-DB and replay migration, round-trip of
|
|
both provider shapes, legacy row reads as `github`.
|
|
- `workflowsync/service_test.go`: dispatch to the right provider, nil-provider
|
|
error text per provider, GitLab fetch → parse → apply, failure recording.
|
|
|
|
Frontend: `lib/utils/gitlab-repo-url.test.ts` (subgroups, self-managed hosts,
|
|
trailing `/-/tree/<branch>/<path>` forms) and a `use-workflow-sync` test for
|
|
provider switching.
|
|
|
|
E2E: not required — no new user-visible flow beyond a form field, and the
|
|
containers project does not cover workflow sync today.
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
make -C apps/backend test
|
|
```
|
|
```bash
|
|
cd apps/backend && golangci-lint run ./... --new-from-rev="$(git merge-base HEAD origin/main)" --timeout=5m
|
|
```
|
|
```bash
|
|
cd apps/web && pnpm run typecheck && pnpm run lint && pnpm vitest run lib/utils lib/api hooks/domains/settings
|
|
```
|
|
```bash
|
|
cd apps && pnpm --filter @kandev/web run i18n:check && pnpm --filter @kandev/web run i18n:ratchet
|
|
```
|
|
|
|
## Risks
|
|
|
|
- **Reconcile-on-switch deletes workflows.** Switching provider makes the new
|
|
file set authoritative; workflows absent from it are deleted. Spec'd
|
|
behavior, but the dialog should warn on provider change. Mitigation: task 06
|
|
includes a confirmation affordance.
|
|
- **Interface churn across four GitLab client impls.** Missing one breaks the
|
|
build, not runtime. Low severity, caught by compilation.
|
|
- **`project_path` URL-encoding.** GitLab needs the full path percent-encoded
|
|
as a single path segment; a naive join silently 404s on subgroups. Covered by
|
|
an explicit nested-path test.
|
|
- **Tree pagination.** A directory with more than the default page size would
|
|
silently truncate the synced set. Explicitly paginated and tested.
|
|
- **i18n ratchet.** New dialog copy must go through `t()` or the pre-commit
|
|
ratchet fails.
|
|
|
|
## Task Waves
|
|
|
|
**Wave 1 — independent, parallel-safe**
|
|
- `task-01-gitlab-repo-content-client.md` — GitLab client methods + impls.
|
|
- `task-02-workflowsync-config-provider.md` — model + schema + validation.
|
|
|
|
**Wave 2 — depends on wave 1**
|
|
- `task-03-gitlab-workspace-repo-contents.md` — service wrappers (needs 01).
|
|
- `task-04-workflowsync-provider-dispatch.md` — interfaces + dispatch (needs 02).
|
|
|
|
**Wave 3 — integration**
|
|
- `task-05-wire-gitlab-provider.md` — DI wiring (needs 03, 04).
|
|
|
|
**Wave 4 — frontend**
|
|
- `task-06-frontend-provider-selector.md` — UI + types + i18n (needs 05).
|
|
|
|
## Status
|
|
|
|
| Task | Status |
|
|
| --- | --- |
|
|
| 01 gitlab-repo-content-client | done |
|
|
| 02 workflowsync-config-provider | done |
|
|
| 03 gitlab-workspace-repo-contents | done |
|
|
| 04 workflowsync-provider-dispatch | done |
|
|
| 05 wire-gitlab-provider | done |
|
|
| 06 frontend-provider-selector | done |
|