Commit dfaab11
feat: full GitLab integration — parity with GitHub (#1120)
* feat(backend): full GitLab parity — store, service, poller, WS, presets
Add the watcher/poll/write-action surface on top of the existing GitLab
client + service skeleton:
* 5 new SQLite tables (mr_watches, review_watches, review_mr_tasks,
issue_watches, issue_watch_tasks, action_presets) + CRUD
* Client + Service: MergeMR, GetProjectMergeMethods, GetProtectedBranch,
ListUserProjects, SearchProjects, SetMRLabels, SetMRAssignees
* Service: review/issue/MR watch CRUD + Check/Trigger, GetStats,
CleanupAllReview/IssueTasks (cleanup-policy aware), TriggerMRSync,
action-presets CRUD with defaults
* Background poller (MR / review / issue loops) wired from main.go
* ~35 GitLab WS action constants + handlers.go registrations
* Mock controller for E2E seeding (MRs, issues, pipelines, discussions,
approvals, branches)
* eventBus + taskDeleter + taskSessionChecker plumbed through Service
* Event types: GitLabMRFeedback / NewReviewMR / NewIssue / TaskMRUpdated
* feat(web): GitLab frontend parity — types, API, store slice, domain hooks
Extend the frontend GitLab integration on top of the new backend surface:
* HTTP API client expansion (gitlab-api.ts): full CRUD for review/issue/MR
watches, MR write actions (merge/approve/labels/assignees), MR
files/commits/feedback, action presets, projects autocomplete, stats
* Store slice expansion: review/issue/MR watches lists, action presets
by workspace, stats + status with loading flags. Properties prefixed
with gitlab to avoid collision with GitHub slice
* Domain hooks: use-gitlab-status, use-gitlab-stats,
use-gitlab-review-watches, use-gitlab-issue-watches,
use-gitlab-action-presets — mirror github hook shapes
* Type model: ReviewWatch, IssueWatch, MRWatch, ActionPresets, Stats,
ProjectMergeMethods, MRApproval, Pipeline, MRFeedback, etc.
* Backend controller HTTP routes for watches/presets/write-actions/
projects/stats so the frontend HTTP API client has endpoints to hit
All passes: backend (build + vet + lint + tests), web (typecheck + lint +
2400 tests).
* fix(gitlab): wire orchestrator event handlers, fix label-filter drop, add tests
Address Claude review blockers on the parity PR:
1. Wire orchestrator subscriptions for GitLab review/issue watch events.
Add event_handlers_gitlab.go with handleGitLabNewReviewMR /
handleGitLabNewIssue, dedup reservation handshake, and task-creator
interfaces (mirrors event_handlers_github.go). main.go now calls
orchestratorSvc.SetGitLabService so the dedup APIs are usable
2. Fix silent label-filter drop: when a custom_query is set on an issue
watch, labels were being appended to the unused filter arg. Fold
labels into customQuery (or default filter) so they actually reach
GitLab; extracted appendLabelsToQuery helper with explicit handling
of pre-existing labels= clauses
3. Switch event payloads to pointer types (mirrors GitHub) so
handlers can type-assert pointer events
4. Backend tests: store_watches_test.go (CRUD + reserve/assign for all
4 new tables + presets), service_watches_test.go (label-filter
helper, cleanup policy, project normalisation), action_presets_test.go
5. Frontend slice tests: review/issue watch CRUD round-trips, action
presets + stats reducer coverage
All green: backend (build+vet+lint+tests), web (typecheck+lint+2403 tests).
* fix(gitlab): address remaining blockers — cleanup wiring, safe defaults, nil guards
Address the second round of Claude review blockers:
1. main.go: wire SetTaskDeleter + SetTaskSessionChecker on services.GitLab
(mirrors GitHub). Without these, manual cleanup sweeps would always
error with "task deleter not configured"
2. service_cleanup.go: default cleanup policy on transient DB error is now
CleanupPolicyNever (preserve tasks) instead of CleanupPolicyAuto
(silently delete). Genuine "watch was deleted" path also falls under
the safe-side default; user can manually delete the orphan tasks
3. service_watches.go: add nil guards on requireStore() so the 6 MR-watch
list/get/delete methods return an errStoreUnavailable error instead
of panicking when the SQLite store failed to construct at boot
4. use-gitlab-status.ts + use-gitlab-stats.ts: add per-mount attemptedRef
so an unreachable GitLab doesn't trigger an infinite re-fetch loop —
the previous useEffect re-ran every render because the failure path
left status null, satisfying the !status guard
* fix(gitlab): propagate session-check errors, clamp poll interval on update
Address remaining Claude blockers:
* service_cleanup.go: HasUserAuthoredMessage transient errors are now
preserved (return false → skip delete) instead of silently ignored;
the alternative was occasionally deleting tasks a user had touched
* service_watches.go: extract clampPollInterval helper applying the same
bounds (0 → default, <60 → 60) as the create path; both
applyReviewWatchPatch and applyIssueWatchPatch use it so user-supplied
zero or tiny values via UpdateXxxWatchRequest no longer hammer GitLab
* fix(gitlab): clear dupl lint, fix preset retry loop + per-workspace watch cache
* service_watches.go: applyReviewWatchPatch / applyIssueWatchPatch get
nolint:dupl markers — they share shape but per-domain field validation
lives in the create paths, so deduplicating via generics would obscure
the contract. Restores backend lint to 0 issues
* use-gitlab-action-presets.ts: per-workspace attemptedRef set so a
failing preset fetch doesn't retry on every render
* use-gitlab-review-watches.ts + use-gitlab-issue-watches.ts: track
lastFetchedRef per consumer so a workspace switch triggers a refetch.
The slice-level loaded flag is shared across instances and can't
double as a per-workspace cache key
* fix(gitlab): add nil-store guards across review/issue watch + cleanup paths
Round-3 Claude finding: ReviewWatch / IssueWatch / preset / reservation
methods still called s.requireStore().X() directly. If NewStore fails at
boot (table migration error), the service struct's store is nil and every
list/get/update/create/reserve method panics on first request.
Apply the same store-nil → errStoreUnavailable pattern from f95a929 to
every remaining method:
* CreateReviewWatch / CreateIssueWatch
* GetReviewWatch / ListReviewWatches / ListAllReviewWatches /
UpdateReviewWatch / TriggerReviewWatchAll
* GetIssueWatch / ListIssueWatches / ListAllIssueWatches /
UpdateIssueWatch / TriggerIssueWatchAll
* ReserveReviewMRTask / AssignReviewMRTaskID / ReleaseReviewMRTask
* ReserveIssueWatchTask / AssignIssueWatchTaskID / ReleaseIssueWatchTask
* lookupReviewPolicy / lookupIssuePolicy (return CleanupPolicyNever)
* fix(gitlab): nil-store guards on CheckXxxWatch + DeleteXxxWatch + cleanup paths
Round-4 Claude finding: 4 remaining panic sites the previous nil-guard
pass missed — these run from the background poller (every 5 min) and the
manual delete/cleanup flows, so a boot-time NewStore failure would
crash the orchestrator process on first tick.
* service_watches.go: add store-nil guards to DeleteReviewWatch /
DeleteIssueWatch / CheckReviewWatch / CheckIssueWatch
* service_cleanup.go: guard CleanupAllReviewTasks / CleanupAllIssueTasks
entry points; wrap the trailing DeleteReviewMRTask / DeleteIssueWatchTask
calls in `if store := s.requireStore(); store != nil` blocks so the
cleanup succeeds even when the dedup-row delete is unreachable
* refactor(gitlab): split service_watches.go to stay under 800-line revive limit
CI's --new-from-rev lint caught service_watches.go at 808 lines (limit
800). Split into three focused files:
* service_events.go (94 LOC) — publish helpers for MR feedback /
new review MR / new issue / watch lifecycle events
* service_issue_watches.go (302 LOC) — Issue watch CRUD + Check +
Trigger + fetch + helpers
* service_reservations.go (59 LOC) — Reserve/Assign/Release dedup
handles used by the orchestrator event handlers
service_watches.go is now 505 lines and contains MR watch + Review
watch only. No behavioral change.
* fix(gitlab): address inline code-review feedback
Address actionable inline comments from Claude/CodeRabbit/cubic/Greptile:
* appendLabelsToQuery: use url.ParseQuery for exact key match instead
of strings.Contains; previously false-matched keys like mylabels=
and silently dropped the watch's labels (test added)
* fetchReviewMRs: drop dead `filter = watch.CustomQuery` assignment —
SearchMRs's buildMRSearchQuery returns customQuery verbatim and
ignores filter when customQuery is non-empty
* store.go: add workspace_id indexes on gitlab_review_watches and
gitlab_issue_watches (the 5-min poller and HTTP list endpoints did
full table scans)
* mock_client.SearchProjects: switch from == to case-insensitive
Contains so partial-query autocomplete returns the seeded project,
matching the doc comment
* Poller: add sync.Mutex to guard `started` field; Start/Stop are
now safe to call concurrently (go test -race would have flagged
the previous read/write race)
* Add ErrWatchNotFound sentinel; controller_watches.go maps it to
HTTP 404 via httpRespondError helper. Update/Trigger handlers
previously returned 500 for missing watches, hiding the distinction
from real server faults
* service_events.go: log Publish errors on watch lifecycle events
instead of swallowing them silently
* handlers.go: reject malformed JSON payloads in list-style WS
handlers rather than silently falling through to broader
list/search behavior
* event_handlers_gitlab.go: remove dead gitlabRepoSlug helper +
blank-identifier suppression
* ci: re-trigger to clear flaky E2E shard 3
* fix(gitlab): address final review round — nil guards, store merge, logs, body validation
- CheckMRWatch, UpdateReviewWatch, UpdateIssueWatch: nil guards for store/req
- handlers: wsNewDiscussionNote rejects empty body
- action_presets: Update reads raw stored row (no default freezing);
Reset validates workspace_id
- service_search.GetStats: log Warn per sub-call failure before zero
- event_handlers_gitlab: log Release* dedup-row errors with context
- web/store.ts: post-slice merge restores every GitLab sub-state field;
extract buildStateOverrides helper
- web/default-state.ts: add GitLab fields to defaultState +
mergeInitialState via mergeGitLabFields helper
- web/gitlab-api.ts: UpdateXxxWatchRequest now Omit workspace_id
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent 6b98185 commit dfaab11
45 files changed
Lines changed: 6417 additions & 135 deletions
File tree
- apps
- backend
- cmd/kandev
- internal
- events
- gitlab
- orchestrator
- pkg/websocket
- web
- hooks/domains/gitlab
- lib
- api/domains
- state
- slices/gitlab
- types
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
747 | 747 | | |
748 | 748 | | |
749 | 749 | | |
750 | | - | |
751 | | - | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
752 | 753 | | |
753 | 754 | | |
754 | 755 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
437 | 438 | | |
438 | 439 | | |
439 | 440 | | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
440 | 453 | | |
441 | 454 | | |
442 | 455 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | | - | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | | - | |
313 | | - | |
314 | 312 | | |
315 | 313 | | |
316 | 314 | | |
317 | 315 | | |
318 | 316 | | |
319 | 317 | | |
320 | | - | |
321 | | - | |
322 | | - | |
| 318 | + | |
323 | 319 | | |
324 | 320 | | |
325 | 321 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
231 | 241 | | |
232 | 242 | | |
233 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
111 | 135 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
| 45 | + | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
| |||
0 commit comments