[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_parent returned 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:

612058-fix2-before-epic_issue_id-null

After: the legacy row exists, so the epic fields are populated:

612058-fix2-after-epic_issue_id-set

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!

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, epics table, not addressed here.
  • Write-path parity in ee/lib/api/epic_issues.rb.
Edited by Daniyal Arshad

Merge request reports

Loading
Loading