Draft: Scope foundational flow preconditions per event type and skip reviewer recommendation when reviewers exist
What does this MR do and why?
MergeRequests::AutoAssignReviewersWorker returns early when a merge request already has a non-automated reviewer, so auto-assignment never overwrites reviewers a human deliberately chose (auto_assign_reviewers_worker.rb:19, 61-69). The DAP trigger path has no equivalent, so marking ready a merge request that already has reviewers launches a Recommend Reviewers run that can churn them. This MR restores that guardrail on the trigger path.
Why a flow precondition isn't enough on its own
The obvious fix is a foundational flow precondition on reviewer counts. It doesn't work, for two reasons:
- Preconditions apply to every event type a flow supports, and
Gitlab::FilterEvaluatoris fail-closed —dig_valueraises on a missing intermediate key andevaluaterescues tofalse. A rule readingreviewers.human_countwould therefore silently block every run of any other event type this flow is configured for, because those events don't supply the field. - The guard is semantically wrong for some events.
recommend_reviewers/v1has nosupported_eventsrestriction, so it can be wired tomention. If someone comments "@duo recommend reviewers" on a merge request that already has reviewers, they've explicitly asked — refusing is the wrong behaviour.
Narrowing supported_events to ready-only would dodge both, but it removes configurability and would make the mention branch of the goal template added in !248388 (merged) dead code. So instead this MR fixes the underlying platform limitation.
Event-scoped preconditions
event_preconditions is a new foundational flow attribute with the same shape as precondition, but keyed by event type name and applied only when that event fires. An event with no entry is left unfiltered rather than blocked.
- The existing flow-wide
preconditionis unchanged and still applies to every event, sofix_pipeline/v1behaves exactly as before. - Both apply when both match, ANDed together with the trigger's own filter.
Ai::FlowTriggers::FilterEvaluator#merge_filterswas generalised from two filters to N; the nesting depth it produces is unchanged, soGitlab::FilterEvaluator::MAX_DEPTHis unaffected.
recommend_reviewers/v1 then declares:
event_preconditions: {
'merge_request_ready' => {
'match' => 'all',
'rules' => [{ 'field' => 'reviewers.human_count', 'operator' => 'eq', 'value' => 0 }]
}
}and the ready worker supplies that field through a new overridable event_filter_data(resource) hook on Ai::CloudEventsFlowTriggerWorker (defaults to {}, so no other worker changes behaviour). A lone Duo Code Review bot counts as "no reviewer yet", matching non_automated_reviewers_present? exactly for every reachable reviewer combination.
Scope: guard 2 is deliberately not included
#607907 (closed) also covers re-fire suppression (every draft ⇄ ready toggle starts a full run). That is not in this MR, deliberately: nothing in the trigger platform dedups concurrent runs for any flow, so it's a platform gap rather than a Recommend Reviewers one, and fixing it here would mean this flow paying for a platform-wide problem. The issue's own AC allows recording the cost as accepted instead. Recommend following up with a dedicated platform issue modelled on TriggerResolveDependencyBumpWorkflowWorker#skip_due_to_workflow_state?, which already does exactly this for one flow.
References
- Related to #607907 (closed) (guard 1 only; guard 2 and the decision record are outstanding)
- Epic: gitlab-org#23019
- Builds on !248388 (merged) (trigger-path additional context and goal)
Notes for review
- Touches the DAP trigger platform, not just our flow:
FilterEvaluator, theFoundationalFlowattribute surface, and the shared worker concern. Worth agroup::duo agent platformlook alongside code review. - Minor conflict with !248388 (merged) — both add keys to the same
recommend_reviewers/v1hash initems.rb. Adjacent but distinct lines; trivial to resolve for whichever lands second. event_preconditionsis not exposed over GraphQL.Types::Ai::FlowTriggerTypeexposesfoundational_flow_preconditiononly, so the UI won't surface this new condition. Left out as it isn't needed for the guard to work — happy to add it if the frontend displays preconditions to users.- Pre-existing quirk found while writing specs (not fixed here):
Gitlab::FilterEvaluator#dig_valueusesvalue[key] || value[key.to_sym], so a legitimatelyfalsefield value falls through to the symbol lookup and reads asnil. That makeseq falserules unmatchable. It's in shared code used by webhooks and integrations, so it's out of scope for this MR, but it's worth its own issue.
Testing
bundle exec rspec ee/spec/services/ai/flow_triggers/filter_evaluator_spec.rb \
ee/spec/workers/ai/catalog/flows/ \
ee/spec/models/ai/catalog/foundational_flow_spec.rb \
ee/spec/models/ai/flow_trigger_spec.rbAll green; RuboCop clean on all changed files. The new examples were confirmed to fail without the production changes (11 failures on master). Coverage includes:
-
Event precondition met / not met / declared for a different event than the one firing (the property that removes the fail-closed trap).
-
Flow-wide and event precondition together, each failing independently.
-
The full guardrail parity matrix ported from
auto_assign_reviewers_worker_spec.rb: no reviewers, lone Duo bot, one human, human + bot. -
End-to-end through
handle_eventwith the realFilterEvaluatorand a realrecommend_reviewers/v1trigger, assertingRunServiceis and isn't invoked. -
All other workers including the shared concern, to confirm the
event_filter_datadefault is a no-op. -
Verify on a real merge request in the playground project before this leaves Draft.
MR acceptance checklist
- Tests added, including the unchanged-behaviour paths for other flows and events
- Follows the code review guidelines
- No documentation change needed (no user-facing config or API surface;
event_preconditionsis an internal flow attribute) - No migration or database review needed