Fix order-dependent epic import gap
What does this MR do and why?
Fix order-dependent epic import gap
Imported epics are built through Gitlab::ImportExport::Base::ObjectBuilder, which caches the instance it builds in the request store for the lifetime of the Sidekiq job. For epics the builder deliberately returns an unsaved instance, because the epic work item and its synced epic are persisted separately by CreateFromImportedEpicService. That means the cached instance is never the record that gets saved, so it stays a new record for the whole job.
Every later reference to the same epic therefore tried to create it a second time, violating the unique index on issues (namespace_id, iid). BulkImports::Pipeline::Runner records that as a soft failure and moves on, so the import reported success while silently dropping every sub-epic under a shared parent and every issue-to-epic link after the first. It reproduces whenever two epics share a parent that is not imported before them, or two issues reference the same epic.
Point the built instance at the row that was persisted, so the three new_record? guards in EpicObjectCreator see the epic that already exists instead of creating another. This keeps the existing find-or-create semantics: an unrelated epic already occupying that iid still fails loudly rather than being silently reused.
Marking the instance persisted also lets NdjsonPipeline#push_placeholder_references see it, which stops user references on epics created through a nested relation from being dropped.
The regression specs set :request_store because the builder only caches while Gitlab::SafeRequestStore is active, which is true in Sidekiq but false in specs by default.
Changelog: fixed EE: true
References
How to set up and validate locally
Note that the order in which you create these objects is important for the reproduction.
Setting up test data
- Create a group
GroupA. - Within
GroupA, create ChildEpicA andChildEpicB. - Within
GroupA, createParentEpicandWorkItemParentEpic. - Set
ChildEpicAandChildEpicB's parents asParentEpic. - Within
GroupA, createProjectA. - Within
ProjectA, createWorkItemAandWorkItemB. - Set
WorkItemAandWorkItemB's parent asWorkItemParentEpic. - Create
GroupDestBefore,GroupDestAfter,ProjectDestBeforeandProjectDestAfter.
Witness the breakage
- Be on
master. - Import
GroupAintoGroupDestBefore. - Import
ProjectAintoProjectDestBefore
curl -s --request POST \
--url "http://gdk.test:3000/api/v4/bulk_imports" \
--header "content-type: application/json" \
--header "PRIVATE-TOKEN: $GITLAB_DEV_TOKEN" \
--data "{
\"configuration\": {
\"url\": \"http://gdk.test:3000\",
\"access_token\": \"$GITLAB_DEV_TOKEN\"
},
\"entities\": [
{
\"source_full_path\": \"GroupA\",
\"source_type\": \"group_entity\",
\"destination_slug\": \"test-001\",
\"destination_namespace\": \"GroupDestBefore\",
\"migrate_projects\": true
}
]
}"
curl -s --request POST \
--url "http://gdk.test:3000/api/v4/bulk_imports" \
--header "content-type: application/json" \
--header "PRIVATE-TOKEN: $GITLAB_DEV_TOKEN" \
--data "{
\"configuration\": {
\"url\": \"http://gdk.test:3000\",
\"access_token\": \"$GITLAB_DEV_TOKEN\"
},
\"entities\": [
{
\"source_full_path\": \"GroupA/ProjectA\",
\"source_type\": \"project_entity\",
\"destination_slug\": \"project-001\",
\"destination_namespace\": \"ProjectDestBefore\",
\"migrate_projects\": true
}
]
}"Witness the errors in the logs:
cat log/importer.log | grep create_from_imported_epic_serviceWitness the missing records:
Repeat the imports into GroupDestAfter and ProjectDestAfter on this branch (after restarting Sidekiq) and witness the errorless logs and the properly created records.
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.


