Reset current_status association after state-change status update
What does this MR do and why?
Fixes a bug where work item status changes triggered by closing or reopening
an issue were missing from the webhook changes payload.
Root cause
EE::Issues::BaseService#update_status_after_state_change delegates the
status write to WorkItems::Widgets::Statuses::UpdateService. That service
calls ensure_work_item, which loads a fresh WorkItem instance when
it receives an Issue. The new status is written to that separate object.
The original issue object retains its stale cached current_status
has-one association.
When the update hook fires (via IssuableBaseService#update with a
state_event: 'close'), hook_association_changes computes the diff:
if supports_status? && (old_current_status(old_associations) != status_with_fallback)
changes[:status] = [...]
endold_associations[:status] was captured before the update (correct old
value). But issue.status_with_fallback reads current_status — which is
still the stale cached old value. old == new, so changes[:status] is
never emitted.
The stale association is directly observable in the Rails console:
issue.status_with_fallback&.name # => "In dev" (stale after close)
issue.association(:current_status).reset
issue.status_with_fallback&.name # => "Complete" (correct)Fix
Add sync_original_association to WorkItems::Widgets::Statuses::UpdateService.
The service stores the original caller as @original and, after writing the
new status, resets @original.association(:current_status) only when the
original object differs from the WorkItem instance the service actually
wrote to. This is detected via object identity (@original.equal?(work_item)),
not a class check - so it's robust to any caller type.
WorkItem callers are the same object the service writes to, so no reset
happens - preserving previous_changes that after_update_commit callbacks
depend on. Issue callers get the reset so the webhook diff builder reads
the new status.
def sync_original_association
return if @original.equal?(work_item)
return unless @original.respond_to?(:current_status)
@original.association(:current_status).reset
endAlternatives considered
- Reset in
base_service.rbwithunless issue.is_a?(WorkItem)guard. This worked but placed knowledge ofUpdateService's internalensure_work_itembehavior in the wrong layer. - Make
ensure_work_itemnot swap instances. Rejected: too much blast radius, the service is called from many places that legitimately pass aWorkItem. - Add status to the
closehook'sold_associations. Wrong locus: theclosehook carries no association diff; the correct fix is at theupdatehook, which already carriesold_associations[:status].
References
- Issue: #604714 (closed)
- Internal adoption epic: &19642
Local validation
The bug only triggers when a plain Issue is passed (not a WorkItem)
because ensure_work_item loads a fresh WorkItem instance only in that
case. The stale association is then observable directly:
project = Project.find_by_full_path('your-group/your-project')
user = project.first_owner
issue = Issue.create!(project: project, title: 'test', author: user)
WorkItems::Statuses::CurrentStatus.create!(work_item_id: issue.id, system_defined_status_id: 2)
issue.association(:current_status).reset
puts issue.status_with_fallback&.name # => "In progress"
# Capture old status — same as associations_before_update does
old_status = issue.status_with_fallback
Issues::CloseService.new(container: project, current_user: user).execute(issue)
# Simulate what hook_association_changes sees when building the webhook diff
hook_data = issue.to_hook_data(user, old_associations: { status: old_status }, action: 'update')
sc = hook_data[:changes][:status]
puts sc ? "previous=#{sc[:previous]['name']} current=#{sc[:current]['name']}" : "NIL - bug present"
# After fix: previous=In progress current=DoneThe reopen path is symmetric — start from a closed issue with a done-category
status and use Issues::ReopenService.
Run the specs:
bundle exec rspec \
ee/spec/services/work_items/widgets/statuses/update_service_spec.rb \
ee/spec/services/work_items/callbacks/status_spec.rb \
ee/spec/services/ee/issues/close_service_spec.rb \
ee/spec/services/ee/issues/reopen_service_spec.rb \
ee/spec/services/ee/issues/update_service_spec.rbMR acceptance checklist
- This MR follows the MR acceptance checklist
- Tests added (update_service_spec, close_service_spec, reopen_service_spec)
- Rubocop clean
- No DB migrations
- EE-only change