Skip redundant epic date system note for non-epic issuables
What does this MR do and why?
When a project-level issue's start_date changes (for example through the workItemUpdate GraphQL mutation), EE::Issuable::CommonSystemNotesService#handle_date_change_note calls SystemNotes::EpicsService#change_epic_date_note. That call is both redundant and broken.
It is redundant because handle_date_change_note was written for legacy Epic records, which store their dates on the epics table, but it fires for any issuable carrying start_date or end_date in previous_changes. On the issues table start_date is a denormalized column kept for efficient sorting — synced from work_item_dates_sources by the DB trigger sync_issues_dates_with_work_item_dates_sources(), and also assigned in Rails by WorkItems::Callbacks::StartAndDueDate — so the check matches Issue and WorkItem too. For those, the CE Issuable::CommonSystemNotesService#handle_start_date_or_due_date_change_note already creates the correct start_date_or_due_date note. (issues has no end_date column, which is why only the start-date half of the check collides.)
It is broken because change_epic_date_note built the note with a hard-coded nil project. That is correct for legacy epics and group-level work items, where Note#ensure_namespace_id derives namespace_id from the noteable, but a project-level issue ends up with neither project_id nor namespace_id. Note.create then fails validation:
Project can't be blank
Exactly one of namespace_id, project_id, organization_id must be present
Project does not match noteable projectSystemNotes::BaseService#create_note swallows the failure and only reports SystemNotes::BaseService::UnpersistedSystemNoteError to error tracking.
This MR adds an early return unless issuable.is_a?(::Epic) to handle_date_change_note, so the epic date note is only created for legacy epics. That resolves the error at its source, since the redundant call is no longer made. Project-level issues keep getting exactly one date note, from the CE handler, as they do today.
Making the note persist instead was the first attempt, and it turned out to be the wrong fix: it produced two near-identical notes on the same issue, start_date_or_due_date ("changed start date to September 10, 2026") and epic_date_changed ("changed start date to Sep 10, 2026"). That surfaced as a failure in spec/services/issuable/common_system_notes_service_spec.rb.
The change to change_epic_date_note — passing noteable.project instead of nil — is retained. Epic#project returns nil, so legacy epics behave exactly as before, and it keeps the service sharding-key-correct for any future caller that passes a project-level noteable.
The Project can't be blank error predates the sharding-key validator added in !251888 (merged), so this is a long-standing bug that the validator made more visible rather than a regression.
Production evidence: Kibana pubsub-rails-inf-gprd, 2026-09-04 18:48 UTC, correlation ID a35f2a829cd901c2-GRU, note_action: epic_date_changed, noteable_type: Issue, caller GraphqlController#execute:workItemUpdate.
References
Closes #627778
Related: !251888 (merged), !253560
Screenshots or screen recordings
Not applicable (backend only).
How to set up and validate locally
- On an EE instance, create a project issue and note its
id. - In a rails console, change its start date through the update service:
issue = Issue.find(<id>) user = issue.author WorkItems::UpdateService.new(container: issue.project, current_user: user, params: { start_date: Date.tomorrow }).execute(issue) issue.notes.system.last - Before this change the issue has one
start_date_or_due_datesystem note andlog/exceptions_json.logcontainsUnpersistedSystemNoteError. After this change that same single note is present withproject_idset andnamespace_idnil, noepic_date_changednote is created, and nothing is logged tolog/exceptions_json.log.
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.