Files
kandev/docs/plans/gitlab-workflow-sync/plan.md
T
2026-08-24 11:09:03 +00:00

7.3 KiB

spec, created, updated, status
spec created updated status
docs/specs/integrations/requirements/gitlab-workflow-sync.md 2026-08-06 2026-08-06 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

make -C apps/backend test
cd apps/backend && golangci-lint run ./... --new-from-rev="$(git merge-base HEAD origin/main)" --timeout=5m
cd apps/web && pnpm run typecheck && pnpm run lint && pnpm vitest run lib/utils lib/api hooks/domains/settings
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