Let flows trigger flows through assignment and cloud events
What does this MR do and why?
In Software Factory, Duo Code Review hands work to Duo Developer by assigning the merge request to it. A status-triggered flow may also change a work item status. With a composite identity token, current_user in these paths is the human the flow acts for. The flow is only visible through the composite identity link. These triggers need a parent workflow so the chain limits from !258724 (merged) apply.
This MR adds a parent workflow to the assign, assign reviewer, and cloud event trigger paths. It is MR 5 of the plan. The change is behind the ai_flow_trigger_chaining feature flag, which is off by default.
This MR builds on !259112 (merged) (merged), where RunService accepts parent_workflow:. It also includes two spec fixes for that MR: the triggered link example now uses a note written by a flow service account (!259112 (comment 3944590180)), and a run with no triggering user is covered (reported by rspec:undercoverage).
Changes:
Gitlab::EventStore::CloudEvent.build_cloud_event(lib/gitlab/event_store/cloud_event.rb):- When the event records
gitlab_composite_actor_id, it now also recordsgitlab_duo_workflow_id. - The value comes from the
X-Gitlab-Duo-Workflow-Idheader, throughGitlab::ApplicationContext. Only integer values are kept. - The field is added to the envelope schema and to the protobuf attributes, so it survives
to_protoandfrom_proto. - !258734 (merged) noted that this field was still missing.
- When the event records
- New
Ai::FlowTriggers::CallerContext.authorized_parent_workflow(workflow_id, user). It loads the workflow and returns it only ifuserhas:update_duo_workflowon it. Both paths below use it..authorized_parent_workflow_for_request(user)reads the id from the header context, and the assignment path andPOST /ai/duo_workflows/workflows(!259111 (merged)) both use it. EE::IssuableBaseService#execute_flow_triggers(issue assign, merge request assign, merge request assign reviewer):- This applies when the flag is enabled for the project's top-level namespace, and the request was made through a composite identity link. That is,
Gitlab::Auth::Identity.resolve_composite_identity_actor(current_user) != current_user. - It resolves the parent from the header context and passes it to
RunServiceasparent_workflow. - Without an authorized parent, it starts no flow and logs
reason: :non_human_trigger_not_permitted. - The existing
trigger_ai_flowcheck stays. - Merge request assignee changes run in
MergeRequests::HandleAssigneesChangeWorker. The header value reaches the worker because Labkit copies the request context into the job (meta.duo_workflow_id) and restores it. This was checked locally.
- This applies when the flag is enabled for the project's top-level namespace, and the request was made through a composite identity link. That is,
Ai::CloudEventsFlowTriggerWorker(work item created and status changed, merge request created, ready, approved, and merged, code conflict):- This applies when the event has a composite actor and the flag is enabled for the container's top-level namespace.
- It authorizes
gitlab_duo_workflow_idagainst the event's user and passes the workflow toRunServiceasparent_workflow. - The event's user is the person whose flow caused the event. Workers can run as someone else: the merged worker runs as the merge request author. The parent is still checked against the event's user, and the chained run then acts for the chain's root human.
- Without an authorized parent, it skips with
reason: :non_human_trigger_not_permittedand logs. - With the flag on, this replaces the handling from !258734 (merged), so
allow_flow_originated_cloud_event_triggersis not used for these events. With the flag off, that handling does not change.
- Human triggers do not change. With the flag off, nothing changes. The only difference is that
parent_workflow: nilis now passed toRunService. - Specs:
spec/lib/gitlab/event_store/cloud_event_spec.rb: workflow id is recorded with a composite identity link; not recorded without a composite actor; not recorded for non-integer values; survives a protobuf round trip.ee/spec/services/ai/flow_triggers/caller_context_spec.rb:.authorized_parent_workflowand.authorized_parent_workflow_for_request.ee/spec/workers/ai/catalog/flows/execute_merge_request_merged_workflow_triggers_worker_spec.rb: the parent is authorized against the merging user, not the author.ee/spec/requests/ai/flow_triggers/flow_originated_status_change_spec.rb: with the flag on, a flow's status change starts flows only with the workflow header; the existing cases run with the flag off.ee/spec/workers/concerns/ai/cloud_events_flow_trigger_worker_spec.rb: parent is passed; a missing, unknown, or unauthorized workflow is skipped and logged; flag off keeps the existing behavior.ee/spec/services/ee/merge_requests/handle_assignees_change_service_spec.rb: parent is passed; a missing or unauthorized workflow is skipped and logged; flag off; a human assignment does not look up a parent.- Existing specs are updated for the new
parent_workflow:argument.
- No changelog, because the change is behind a default-off flag. No docs, because docs are MR 6 of the plan.
Known limitations
- The plan's end-to-end specs for this MR are not included. They cover Code Review assigning to Developer and back until the depth limit stops it, and a status-triggered flow that changes the same work item's status until the limit stops it. Depth and root checks are covered in the
RunServiceandCreateWorkflowServicespecs. End-to-end coverage is tracked in #631755. - As in !259111 (merged) and !259112 (merged),
:update_duo_workflowproves that the user may update the named workflow. It does not prove that the request came from that workflow. This is tracked on the plan. - Callers must send the
X-Gitlab-Duo-Workflow-Idheader. With the flag on, a flow request without it starts no flow. - Logging and internal events for chain rejections are MR 6 of the plan.
References
- Plan: https://gitlab.com/gitlab-org/gitlab/-/work_items/630476
- MR 0, cloud events caused by a flow skip flow triggers: !258734 (merged)
- MR 1,
parent_workflow_idcolumn andflowtrigger source: !258704 (merged) - MR 2,
Ai::FlowTriggers::CallerContextand chain limits inCreateWorkflowService: !258724 (merged) - MR 4,
RunServiceacceptsparent_workflow:: !259112 (merged)
Screenshots or screen recordings
Not applicable. This is a backend-only change.
How to set up and validate locally
-
In the Rails console, enable the flag:
Feature.enable(:ai_flow_trigger_chaining) -
Set up an assign flow trigger for a flow service account on a project.
-
From a running flow, assign a merge request to that service account. Use the flow's composite token and the
X-Gitlab-Duo-Workflow-Idheader. -
Check that:
- The assigned flow starts.
- Its workflow has
parent_workflow_idset to the first workflow, andtrigger_sourceset toflow. - Its
useris the human who started the chain.
-
Repeat the steps by changing the status of a work item that has a status flow trigger.
-
Run the specs:
bin/rspec spec/lib/gitlab/event_store/cloud_event_spec.rb ee/spec/services/ai/flow_triggers/caller_context_spec.rb ee/spec/workers/concerns/ai/cloud_events_flow_trigger_worker_spec.rb ee/spec/services/ee/merge_requests/handle_assignees_change_service_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.