fix(gitlab): scope MR automation switches per linked MR (#2676)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-08-22 14:44:56 +00:00
committed by GitHub
parent 6d7e4e8698
commit ecda06748e
58 changed files with 3821 additions and 711 deletions
@@ -75,6 +75,22 @@ dispatch functions turned out byte-for-byte identical below the decision layer.
- **`locked` state.** GitLab's MR state machine has a fourth value with no GitHub
analogue. It is treated as non-terminal (fires neither `merged` nor `closed`) and
is exercised as its own regression case in the decision-function tests.
- **Amended: the five switches are now per linked MR, not per task.** As written,
this ADR put `prompt_on_review_requested` / `prompt_on_merged` / `prompt_on_closed`
(and later auto-fix / auto-merge) on `gitlab_task_mr_options`, keyed by `task_id`
alone. That is wrong for a task with more than one linked MR: enabling a switch on
one MR silently enabled it on all of them. The five switches moved to
`gitlab_task_mr_automation_options`, keyed by the same
`(task_id, repository_id, project_path, mr_iid)` identity as
`gitlab_task_mr_state`, seeded once from the legacy task-wide values by an
`mr_scope_migrated_at`-guarded fan-out. `gitlab_task_mr_options` keeps only what is
genuinely task-level — the auto-fix prompt override and the server-resolved
reviewer username — so a reviewer-identity change still clears every linked MR's
review-request baseline at once, while a switch flip clears only its own MR's
checkpoints. A `PATCH` / `update_task_mr_automation_kandev` call naming an MR
targets it alone; omitting MR identity fans out to every linked MR, which preserves
the behavior of agents that have no MR identity to send. See
`docs/specs/gitlab-integration/spec.md`'s "MR automation" section.
- **Discovered while integrating:** `executeQueuedMessage`'s lifecycle-prompt
detection (`event_handlers_agent.go`) was hardcoded to the GitHub PR automation
origin string. A GitLab-originated durable lifecycle entry was queued but never
+3 -1
View File
@@ -372,7 +372,9 @@ These actions use the connected GitLab user's permissions and do not bypass prot
### Automate a linked merge request
For a task with a linked GitLab merge request, open the MR topbar control. The **Automation** group has the same two task-level controls as GitHub's PRs: **Auto-fix CI and address comments** and **Auto-merge when ready**. Below it, expand **Review follow-up** for three lifecycle booleans: **Your review is requested**, **MR merged**, and **MR closed without merging**. Enabling any control applies it to every MR linked to that task; Kandev tracks delivery and deduplication separately for each linked MR.
For a task with a linked GitLab merge request, open the MR topbar control. The **Automation** group has the same two controls as GitHub's PRs: **Auto-fix CI and address comments** and **Auto-merge when ready**. Below it, expand **Review follow-up** for three lifecycle booleans: **Your review is requested**, **MR merged**, and **MR closed without merging**.
All five belong to a single merge request. A task with several linked MRs shows one **Automation** group per MR, each labelled with its MR number, so you can automate one MR and leave the rest untouched; Kandev tracks delivery and deduplication separately for each. The auto-fix prompt override is the one setting that stays task-level; editing it applies to every linked MR. An agent calling `update_task_mr_automation_kandev` can name a merge request to target it alone, or omit the merge-request fields to apply the change to every MR linked to the task.
Kandev reuses the existing lightweight task MR poller, which checks linked MRs roughly once per minute; it does not add a separate scheduler. Saving enabled options also evaluates the task's current linked MRs without waiting for the next poll.
+1 -1
View File
@@ -274,7 +274,7 @@ The GitLab MR topbar control has an **Automation** group with the same two actio
- **Auto-fix CI and address comments** sends the agent a new or changed failing pipeline job or unresolved discussion note once the pipeline settles, and stops after 10 repair rounds for that MR. Disable and re-enable it to reset the limit.
- **Auto-merge when ready** merges only after the pipeline passes, unresolved discussions are cleared, and GitLab's own merge-readiness check agrees.
Below that, open **Review follow-up** for the same three notification switches GitHub uses, task-level and applying to every merge request linked to the task:
Below that, open **Review follow-up** for the same three notification switches GitHub uses. Every switch above belongs to one merge request: a task with several linked MRs shows an **Automation** group per MR, each labelled with its MR number, and turning a switch on for one leaves the others alone.
- **Your review is requested** wakes the agent when the workspace's connected GitLab account is newly added as a reviewer on the MR. Staying assigned across MR updates does not re-fire it; being removed and re-added (for example, for a re-review after changes) does.
- **MR merged** and **MR closed without merging** independently wake the agent when review work ends.
+47 -23
View File
@@ -108,16 +108,18 @@ workflows are not usable end to end.
switches — `Auto-fix CI and address comments` and `Auto-merge when ready` —
above a collapsible `Review follow-up` group holding the three lifecycle
notification switches (`Your review is requested`, `MR merged`, `MR closed
without merging`) introduced for MR lifecycle notifications. These switches
currently use task-level settings, so changing one linked MR's control also
affects the task's other linked MRs. Per-MR switch scoping, including
per-MR round/attempt state and identity-aware `PATCH`/MCP updates, is tracked
in the "Scope GitLab MR automation switches per MR" follow-up task. The
auto-fix prompt override remains task-level. See "Automation (lifecycle,
auto-fix, auto-merge)" below.
without merging`) introduced for MR lifecycle notifications. All five
switches are scoped per linked MR (see the per-MR amendment in
[the GitLab MR lifecycle ADR](../../decisions/2026-08-01-gitlab-mr-lifecycle-notifications.md)):
enabling a switch on one linked MR does not affect
any other linked MR's switches, and a `PATCH`/MCP update that omits MR
identity fans out to every linked MR, preserving prior agent behavior.
Auto-fix and auto-merge additionally track per-MR round/attempt state. The
auto-fix prompt override remains task-level, as does the resolved review
reviewer username. See "Automation (lifecycle, auto-fix, auto-merge)" below.
- `Auto-fix CI and address comments` sends or queues an agent prompt when a
linked MR's pipeline has a new or changed failing job, or a new or changed
unresolved discussion note, capped at 10 accepted rounds per task.
unresolved discussion note, capped at 10 accepted rounds per linked MR.
`Auto-merge when ready` merges a linked MR only when it is open, not a
draft, its pipeline succeeded, it has zero unresolved discussions, and
GitLab's own merge-readiness verdict agrees.
@@ -130,8 +132,11 @@ workflows are not usable end to end.
Clicking the button opens the MR detail panel directly (no intermediate
dropdown), also mirroring GitHub's single-PR topbar button. A task with
2+ linked MRs, and touch/coarse-pointer surfaces regardless of MR count,
keep the click-only dropdown (per-MR review/open/unlink rows, the
Automation group, and "Link another merge request") with no hover popover.
keep the click-only dropdown (per-MR review/open/unlink rows, one
Automation block per linked MR — each labeled with that MR's number, its
auto-fix/auto-merge rows always visible and its nested `Review follow-up`
group collapsed unless one of that MR's own three lifecycle switches is
already on — and "Link another merge request") with no hover popover.
- The Kanban card shows a merge-request badge (`IconGitMerge`, coloured by
state/pipeline/approval) next to the existing pull-request badge when the
task has at least one linked MR. Multiple linked MRs collapse into one badge
@@ -184,12 +189,23 @@ into the secret store.
notable transitions.
- GitLab notification subscription state is owned by GitLab. Kandev reads it
live and does not duplicate it in SQLite.
- `gitlab_task_mr_options` is a per-task row: `task_id` (PK), the three
lifecycle booleans (`prompt_on_review_requested`, `prompt_on_merged`,
`prompt_on_closed`), `review_reviewer_username`, `auto_fix_enabled`,
`auto_merge_enabled`, `auto_fix_prompt_override` (nullable; empty/`NULL`
means use the built-in `mr-auto-fix` prompt), and timestamps. One row covers
every MR linked to the task.
- `gitlab_task_mr_options` is a per-task row: `task_id` (PK), the genuinely
task-level fields `review_reviewer_username` and `auto_fix_prompt_override`
(nullable; empty/`NULL` means use the built-in `mr-auto-fix` prompt),
`mr_scope_migrated_at` (nullable; guards the one-time fan-out into
`gitlab_task_mr_automation_options` below so a replay never re-enables a
switch a user has since turned off for one MR), and timestamps. Its five
boolean columns (`auto_fix_enabled`, `auto_merge_enabled`,
`prompt_on_review_requested`, `prompt_on_merged`, `prompt_on_closed`) are
legacy: no longer written, read only by that one-time migration.
- `gitlab_task_mr_automation_options` is the per-MR source of truth for the
five automation switches, keyed by `(task_id, repository_id, project_path,
mr_iid)`. A `PATCH`/MCP update naming one linked MR's identity writes only
that row; omitting MR identity fans the patch out to every row currently
linked to the task. The public `GET` response's top-level switch booleans
stay an aggregate ("on for every linked MR, and at least one MR linked")
for MCP/API read compatibility; the `mr_options` array in that response is
the per-MR source of truth the UI renders from.
- `gitlab_task_mr_state` is a per-`(task_id, repository_id, project_path,
mr_iid)` row carrying lifecycle dedupe fields (from MR lifecycle
notifications) plus `last_fix_signature`, `last_fix_checkpoint_json`,
@@ -265,12 +281,16 @@ validated against any supplied value.
### MR automation (lifecycle, auto-fix, auto-merge)
- `GET /tasks/:taskID/mr-automation` returns the task's `TaskMRAutomationOptions`:
the three lifecycle booleans, `review_reviewer_username`, `auto_fix_enabled`,
`auto_merge_enabled`, `auto_fix_prompt_override` (`null` when unset),
`auto_fix_max_rounds` (`10`), `effective_auto_fix_prompt`,
`using_default_prompt`, `updated_at`, and `mr_states` (one
`TaskMRLifecycleState` per linked MR, carrying both the lifecycle dedupe
fields and the auto-fix/auto-merge checkpoint fields).
`automation_revision`, the three lifecycle booleans,
`review_reviewer_username`, `auto_fix_enabled`, `auto_merge_enabled`,
`auto_fix_prompt_override` (`null` when unset), `auto_fix_max_rounds` (`10`),
`effective_auto_fix_prompt`, `using_default_prompt`, `updated_at`,
`mr_options` (one row of the five switches per linked MR), and `mr_states`
(one `TaskMRLifecycleState` per linked MR, carrying both the lifecycle dedupe
fields and the auto-fix/auto-merge checkpoint fields). The top-level switch
booleans are compatibility aggregates: they are true only when at least one
MR is linked and every linked MR has the switch enabled. Clients that need
one MR's exact value must read its `mr_options` row.
- `PATCH /tasks/:taskID/mr-automation` accepts a partial body with any of the
same fields (excluding `auto_fix_max_rounds`, `effective_auto_fix_prompt`,
`using_default_prompt`, `updated_at`, and `mr_states`, which are
@@ -278,7 +298,11 @@ validated against any supplied value.
field`; `auto_fix_enabled`/`auto_merge_enabled`/the three lifecycle booleans
reject an explicit `null` (they are switches, not clearable values);
`auto_fix_prompt_override: null` or `""` restores the built-in `mr-auto-fix`
prompt.
prompt. To target one linked MR, clients pass the complete
`repository_id`, `project_path`, and `mr_iid` selector tuple. If all three
selectors are omitted, the switch patch fans out to every linked MR. A
partial selector tuple or an unlinked MR returns `400` without a write.
The prompt override remains task-level and does not use these selectors.
- Current-task MCP exposes `get_task_mr_automation_kandev` and
`update_task_mr_automation_kandev` with the same shape, scoped to the
connected task.