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 records gitlab_duo_workflow_id.
    • The value comes from the X-Gitlab-Duo-Workflow-Id header, through Gitlab::ApplicationContext. Only integer values are kept.
    • The field is added to the envelope schema and to the protobuf attributes, so it survives to_proto and from_proto.
    • !258734 (merged) noted that this field was still missing.
  • New Ai::FlowTriggers::CallerContext.authorized_parent_workflow(workflow_id, user). It loads the workflow and returns it only if user has :update_duo_workflow on it. Both paths below use it. .authorized_parent_workflow_for_request(user) reads the id from the header context, and the assignment path and POST /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 RunService as parent_workflow.
    • Without an authorized parent, it starts no flow and logs reason: :non_human_trigger_not_permitted.
    • The existing trigger_ai_flow check 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.
  • 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_id against the event's user and passes the workflow to RunService as parent_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_permitted and logs.
    • With the flag on, this replaces the handling from !258734 (merged), so allow_flow_originated_cloud_event_triggers is 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: nil is now passed to RunService.
  • 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_workflow and .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 RunService and CreateWorkflowService specs. End-to-end coverage is tracked in #631755.
  • As in !259111 (merged) and !259112 (merged), :update_duo_workflow proves 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-Id header. 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

Screenshots or screen recordings

Not applicable. This is a backend-only change.

How to set up and validate locally

  1. In the Rails console, enable the flag:

    Feature.enable(:ai_flow_trigger_chaining)
  2. Set up an assign flow trigger for a flow service account on a project.

  3. From a running flow, assign a merge request to that service account. Use the flow's composite token and the X-Gitlab-Duo-Workflow-Id header.

  4. Check that:

    • The assigned flow starts.
    • Its workflow has parent_workflow_id set to the first workflow, and trigger_source set to flow.
    • Its user is the human who started the chain.
  5. Repeat the steps by changing the status of a work item that has a status flow trigger.

  6. 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.

Edited by Igor Drozdov

Merge request reports

Loading
Loading