Link restarted Duo sessions to their source workflow
What does this MR do and why?
Adds workflow-to-workflow linking for Duo Workflow sessions, mirroring the existing artifact linking in Ai::DuoWorkflows::LinkArtifactService (work items, merge requests, notes, pipelines) - see #600655 (closed)
- New
duo_workflows_workflow_workflowsjoin table +Ai::DuoWorkflows::WorkflowWorkflowjoin model (via the sharedWorkflowLinkableconcern), with a singlelink_typefor now:source— the workflow whose restart produced this run. Ai::DuoWorkflows::RestartWorkflowServicenow creates the link after a workflow is restartedWorkflowgainsworkflow_links/linked_workflowsassociations alongside the existing artifact link associations.
The schema mirrors the other join tables: unique index on (workflow_id, linked_workflow_id, link_type) (which also makes ensure_link's upsert race-safe), cascading foreign keys on workflow_id, linked_workflow_id, project_id, and namespace_id, and the project-xor-namespace check constraint.
References
How to set up and validate locally
Database
Up:
for v in 20260917120000 20260917120001 20260917120002 20260917120003 20260917120004; do
bundle exec rails db:migrate:up:main VERSION=$v
doneLocal Output
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 3516
main: == 20260917120000 CreateDuoWorkflowsWorkflowWorkflows: migrating ==============
main: -- create_table(:duo_workflows_workflow_workflows, {:if_not_exists=>true})
main: -> 0.0157s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows\nADD CONSTRAINT check_c8fe093d47\nCHECK ( num_nonnulls(namespace_id, project_id) = 1 )\nNOT VALID;\n")
main: -> 0.0008s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0461s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows VALIDATE CONSTRAINT check_c8fe093d47;")
main: -> 0.0011s
main: -- execute("RESET statement_timeout")
main: -> 0.0005s
main: == 20260917120000 CreateDuoWorkflowsWorkflowWorkflows: migrated (0.1320s) =====
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 3516
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 3796
main: == 20260917120001 AddFkDuoWorkflowsWorkflowWorkflowsToWorkflows: migrating ====
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE duo_workflows_workflows, duo_workflows_workflow_workflows IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0005s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows ADD CONSTRAINT fk_6ab757d7b9 FOREIGN KEY (workflow_id) REFERENCES duo_workflows_workflows (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0040s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows VALIDATE CONSTRAINT fk_6ab757d7b9;")
main: -> 0.0060s
main: -- execute("RESET statement_timeout")
main: -> 0.0397s
main: == 20260917120001 AddFkDuoWorkflowsWorkflowWorkflowsToWorkflows: migrated (0.1241s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 3796
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4016
main: == 20260917120002 AddFkDuoWorkflowsWorkflowWorkflowsToLinkedWorkflows: migrating
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE duo_workflows_workflows, duo_workflows_workflow_workflows IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0006s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows ADD CONSTRAINT fk_ce0416f6b6 FOREIGN KEY (linked_workflow_id) REFERENCES duo_workflows_workflows (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0020s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows VALIDATE CONSTRAINT fk_ce0416f6b6;")
main: -> 0.0037s
main: -- execute("RESET statement_timeout")
main: -> 0.0473s
main: == 20260917120002 AddFkDuoWorkflowsWorkflowWorkflowsToLinkedWorkflows: migrated (0.1463s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4016
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4202
main: == 20260917120003 AddFkDuoWorkflowsWorkflowWorkflowsToProjects: migrating =====
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE projects, duo_workflows_workflow_workflows IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0005s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows ADD CONSTRAINT fk_ca85eb899a FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0026s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows VALIDATE CONSTRAINT fk_ca85eb899a;")
main: -> 0.0091s
main: -- execute("RESET statement_timeout")
main: -> 0.0005s
main: == 20260917120003 AddFkDuoWorkflowsWorkflowWorkflowsToProjects: migrated (0.0966s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4202
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4412
main: == 20260917120004 AddFkDuoWorkflowsWorkflowWorkflowsToNamespaces: migrating ===
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- execute("LOCK TABLE namespaces, duo_workflows_workflow_workflows IN SHARE ROW EXCLUSIVE MODE")
main: -> 0.0006s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows ADD CONSTRAINT fk_b57557c3fa FOREIGN KEY (namespace_id) REFERENCES namespaces (id) ON DELETE CASCADE NOT VALID;")
main: -> 0.0025s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- execute("ALTER TABLE duo_workflows_workflow_workflows VALIDATE CONSTRAINT fk_b57557c3fa;")
main: -> 0.0079s
main: -- execute("RESET statement_timeout")
main: -> 0.0005s
main: == 20260917120004 AddFkDuoWorkflowsWorkflowWorkflowsToNamespaces: migrated (0.0982s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 4412
Pro tip: try out scripts/database/migrate.rb for better migration management.Down:
for v in 20260917120004 20260917120003 20260917120002 20260917120001 20260917120000; do
bundle exec rails db:migrate:down:main VERSION=$v
doneLocal Output
main: == [advisory_lock_connection] object_id: 165040, pg_backend_pid: 70185
main: == 20260917120004 AddFkDuoWorkflowsWorkflowWorkflowsToNamespaces: reverting ===
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:duo_workflows_workflow_workflows, {:column=>:namespace_id})
main: -> 0.0057s
main: == 20260917120004 AddFkDuoWorkflowsWorkflowWorkflowsToNamespaces: reverted (0.1174s)
main: == [advisory_lock_connection] object_id: 165040, pg_backend_pid: 70185
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70402
main: == 20260917120003 AddFkDuoWorkflowsWorkflowWorkflowsToProjects: reverting =====
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:duo_workflows_workflow_workflows, {:column=>:project_id})
main: -> 0.0031s
main: == 20260917120003 AddFkDuoWorkflowsWorkflowWorkflowsToProjects: reverted (0.0946s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70402
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70620
main: == 20260917120002 AddFkDuoWorkflowsWorkflowWorkflowsToLinkedWorkflows: reverting
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:duo_workflows_workflow_workflows, {:column=>:linked_workflow_id})
main: -> 0.0027s
main: == 20260917120002 AddFkDuoWorkflowsWorkflowWorkflowsToLinkedWorkflows: reverted (0.0891s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70620
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70846
main: == 20260917120001 AddFkDuoWorkflowsWorkflowWorkflowsToWorkflows: reverting ====
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- remove_foreign_key(:duo_workflows_workflow_workflows, {:column=>:workflow_id})
main: -> 0.0031s
main: == 20260917120001 AddFkDuoWorkflowsWorkflowWorkflowsToWorkflows: reverted (0.0810s)
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 70846
Pro tip: try out scripts/database/migrate.rb for better migration management.
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 71028
main: == 20260917120000 CreateDuoWorkflowsWorkflowWorkflows: reverting ==============
main: -- drop_table(:duo_workflows_workflow_workflows)
main: -> 0.0089s
main: == 20260917120000 CreateDuoWorkflowsWorkflowWorkflows: reverted (0.0175s) =====
main: == [advisory_lock_connection] object_id: 165060, pg_backend_pid: 71028
Pro tip: try out scripts/database/migrate.rb for better migration management.Testing Instructions:
-
Run a developer flow locally that takes a while (giving you enough time to cancel it). e.g.
@duo-developer-gitlab-duo - Wait 2 min and then check the time and report back. -
On the right sidebar, open the session and cancel it. Note the workflow ID.
-
From the command line, restart the session:
curl --request POST \
--header "PRIVATE-TOKEN: <GDK_PAT_TOKEN>" \
--url "http://gdk.test:3000/api/v4/ai/duo_workflows/workflows/<WORKFLOW_ID>/restart"- Confirm the link from this new workflow to the previous workflow exists in the console, with link_type of
source
pry(main)> w = Ai::DuoWorkflows::Workflow.last
=> #<Ai::DuoWorkflows::Workflow:0x0000000147410d00
id: 4883, ...
pry(main)> w.linked_workflows
=> [#<Ai::DuoWorkflows::Workflow:0x00000001378d4388
id: 4882, ...
pry(main)> w.workflow_links
=> [#<Ai::DuoWorkflows::WorkflowWorkflow:0x0000000146712478
id: 1,
workflow_id: 4883,
linked_workflow_id: 4882,
project_id: 22,
namespace_id: nil,
created_at: "2026-09-23 19:38:39.324582000 +0000",
updated_at: "2026-09-23 19:38:39.324582000 +0000",
link_type: "source">]MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.