[Fix] Prevent parent links creation without a legacy epic_issues row
What does this MR do and why?
Two write paths create a work_item_parent_links row without the legacy epic_issues row it is supposed to be bridged to. That inconsistency is what produced the 500s in #612058. !249710 (merged) made the API tolerate the gap; this MR stops it from being created.
EE::WorkItems::ParentLinks::ReorderService#move_synced_object!looked up the legacy row instead of building one, so adding an issue to an epic with a position skipped the insert. It returned nil, so the caller also skipped the system note and the GraphQL update trigger: no row, no note, no error, nothing in the logs.EE::WorkItems::DataSync::Widgets::Hierarchy#copy_issue_parentreturned early when the source issue had no legacy row, so moving an affected issue re-created the gap on the target. Any backfill of the existing rows would decay over time.
Both now build the row with EpicIssue.find_or_initialize_from_parent_link, the helper ParentLinks::CreateService already uses. Epic children and promotions are untouched, they have no epic_issues row by design.
Worth noting that the gap is wider than epic_issue_id: everything the REST API derives from the legacy row disappears with it, so the child reports no epic at all.
Screenshots
GET /groups/:id/epics/:epic_iid/issues?per_page=1 for a child added to an epic with a position. Same request, only the code differs.
Before: epic_iid, epic and epic_issue_id are all null:
After: the legacy row exists, so the epic fields are populated:
How to validate locally
On a GDK with an Ultimate license, create a group-level epic and two issues in a project under that group. Add the first issue to the epic normally, then add the second one with a position:
WorkItems::UpdateService.new(
container: second_issue.project, current_user: current_user, params: {},
widget_params: { hierarchy_widget: {
parent: epic_work_item, adjacent_work_item: first_issue.reset, relative_position: 'BEFORE'
} }
).execute(second_issue)second_issue.reset.parent_link.epic_issue is nil before this change and present after, and the epic gets its system note back.
For the second path, hand-create an affected link, then move the issue to another project with WorkItems::DataSync::MoveService and check the target's parent_link.epic_issue:
link = WorkItems::ParentLink.new(work_item: WorkItem.find(third_issue.id), work_item_parent: epic_work_item)
link.work_item_syncing = true
link.save!Related to
https://gitlab.com/gitlab-org/gitlab/-/issues/612058
Follow-ups
- Backfilling the 185 rows that are already inconsistent instance-wide.
- Promoting an affected issue still leaves the promoted legacy epic without
parent_id/work_item_parent_link_id: same gap,epicstable, not addressed here. - Write-path parity in
ee/lib/api/epic_issues.rb.

