Start Duo Code Review flow out of band
What does this MR do and why?
Duo Code Review is itself a Duo flow. Starting it authenticates as the Code Review service account, which links a composite identity to the request. A request can carry only one composite identity.
When a review is requested from inside a request that already acts as another Duo flow's service account, such as a merge request created or un-drafted by a Duo flow, linking the second identity fails and no review runs.
This MR starts the review from a Sidekiq worker that carries no composite identity, so the review's identity link no longer competes with whatever identity the enqueuing request already carries.
Changes
- New worker
Ai::DuoWorkflows::CodeReview::StartReviewWorker, which starts the flow with no composite identity linked. EE::MergeRequests::BaseService#start_duo_code_reviewenqueues that worker instead of starting inline, behind the feature flag.- The billable human is resolved at enqueue time, because the worker has no identity to resolve from.
MergeRequest#duo_code_review_in_flight?keeps the worker idempotent under Sidekiq's at-least-once delivery.MergeRequest#duo_code_review_startable?is now shared by the request path and the worker.CreateAndStartWorkflowServicemarks a workflow failed when its start does not succeed, instead of leaving it looking live, and emits the terminal event and audit record thatCleanStuckWorkflowsServiceused to emit half an hour later.
The last item touches a service shared by all Duo flows, not just Duo Code Review. It has three callers: CodeReview::ReviewMergeRequestService, RecommendReviewers::ExecuteService and GenerateWorkplanService.
Each of these carries an inline comment on the diff explaining why it is the way it is.
Feature flag
- Name:
duo_code_review_clean_identity_start - Type:
gitlab_com_derisk,default_enabled: false - Checked against the merge request's project.
- Gates the transport. With the flag off, the review starts inline exactly as before.
One change is deliberately not gated, because it is not specific to Duo Code Review: a workflow whose start fails now moves to failed at once rather than sitting at created until CleanStuckWorkflowsService reaps it. The terminal state and the records emitted are the same as before, they just arrive at the point of failure instead of thirty minutes later. duo_code_review_in_flight? depends on this, because it matches created and running, so without it a failed start would block every later review request on that merge request until the reaper ran.
References
How to test
Prerequisites
-
A GDK with Duo Agent Platform working, and an online CI runner tagged
gitlab--duo. Duo flows sit at "Starting job" without one. -
A project with Duo features and foundational flows enabled, and "Automatically request Duo Code Review" enabled in the project settings.
-
The project's numeric ID, shown on the project overview page. You paste it into the flow definition below.
-
Restart Sidekiq after checking out this branch, because GDK's Sidekiq does not reload changed code:
gdk restart rails-background-jobs
Step 1: create the probe flow
To reproduce the original bug you need a request that already carries a composite identity before the review is requested. This flow acts as "another Duo flow": it takes a merge request out of draft using its own service account, and that action is what causes Duo Code Review to be requested from inside an already identity-carrying request.
- Go to your project, then Automate, then Flows, then New flow.
- Give it a name, for example
PROBE DCR undraft. - Paste the definition below, replacing
PID=1000000with your own project's numeric ID. - Save the flow, then press Enable on it so a service account is provisioned for the project.
- Note the service account username that gets created. It looks like
ai-probe-dcr-undraft-<group>.
version: "v1"
environment: ambient
components:
- name: "capture_goal"
type: DeterministicStepComponent
tool_name: "create_file_with_contents"
inputs:
- from: "context:goal"
as: "contents"
- from: "probe_goal.txt"
as: "file_path"
literal: true
ui_log_events:
- "on_tool_execution_success"
- "on_tool_execution_failed"
- name: "probe"
type: DeterministicStepComponent
tool_name: "run_command"
inputs:
- from: >-
echo "=====PROBE_BEGIN=====";
PID=1000000;
IID=$(sed -n 's/.*MergeRequest IID: \([0-9][0-9]*\).*/\1/p' probe_goal.txt | head -1);
echo "--- target MR: !$IID";
P="projects/$PID/merge_requests/$IID";
echo "--- draft before:"; glab api $P 2>&1 | grep -o '"draft":[a-z]*' | head -1;
T=$(glab api $P 2>&1 | grep -o '"title":"[^"]*"' | head -1 | sed 's/^"title":"//; s/"$//; s/^Draft: *//');
echo "--- un-draft:"; glab api --method PUT $P -f title="$T" 2>&1 | grep -o '"draft":[a-z]*' | head -1;
sleep 40;
echo "--- notes:"; glab api "$P/notes?per_page=30&sort=desc" 2>&1 | grep -o '"body":"[^"]\{0,240\}"';
echo "=====PROBE_END====="
as: "command"
literal: true
ui_log_events:
- "on_tool_execution_success"
- "on_tool_execution_failed"
routers:
- from: "capture_goal"
to: "probe"
- from: "probe"
to: "end"
flow:
entry_point: "capture_goal"A mention on a merge request hands the flow a goal containing Context: {MergeRequest IID: 17}. The first component writes that goal to a file, and the second reads the IID out of it and takes that merge request out of draft. This is why the flow acts on whichever merge request you mention it on.
Step 2: add a Mention trigger
- Go to Automate, then Triggers, then New trigger.
- Point it at the flow you created, and choose the Mention event.
Step 3: run it
-
Create or reopen a merge request in that project. It must be a Draft, have a non-empty diff, and not already have GitLab Duo as a reviewer.
-
Comment on it, mentioning the flow's service account, for example:
@ai-probe-dcr-undraft-<group> please run -
The flow starts and takes the merge request out of draft.
Expected results
With the flag disabled:
- The merge request is still taken out of draft.
- GitLab Duo is still auto-assigned as reviewer.
- No review runs. GitLab Duo posts an error note instead, carrying error code
DCR5000.
| Un-drafted by a human | Un-drafted by service account (the PROBE DCR undraft flow) |
|---|---|
![]() |
![]() |
With the flag enabled for the project:
Feature.enable(:duo_code_review_clean_identity_start, Project.find_by_full_path('group/project'))- The merge request is taken out of draft.
- GitLab Duo is auto-assigned as reviewer.
- A system note appears saying a review session started, and the review runs.
| Un-drafted by a human | Un-drafted by service account (the PROBE DCR undraft flow) |
|---|---|
![]() |
![]() |
Enabling a flag for a project actor shows as "off" on the global toggle in the admin UI. That is expected, because the actor gate is what applies, not the global one.
Also worth testing
- Un-draft a merge request as a human, with the flag both on and off. Both should produce a review.
- Cancel a running review session from Automate, then unassign and reassign GitLab Duo. A new review session should start.



