Draft: Record deleted label events in the label audit event

What does this MR do and why?

Part of the stacked chain fixing work item 628135 (INC-12449). MR 4 in this chain made Label destroy delete resource_label_events rows instead of nullifying label_id, so the timeline entry that once read "added bug" now disappears from the issue activity feed with no way back. This MR records that loss in the audit trail.

BatchDeleteDependentAssociations now counts how many rows each dependent association loses during delete_dependent_associations_in_batches and exposes the tally through a new deleted_dependent_association_counts reader. Counts reset at the start of each run.

EE::Labels::DestroyService reads the resource_label_events count off the label after destroy and adds it to the existing label_deleted audit event (added in 18.11, no new event type here): in the human readable message, for example "Deleted label Used and 2 timeline entries recording its use", and in additional_details as resource_label_events_deleted.

Gaps worth flagging: audit events are EE only, so CE gets no record of the deleted timeline entries. The CASCADE foreign key MR 4 added at the database level bypasses this service entirely, so a cascade triggered outside Labels::DestroyService is not audited. Any other code path that deletes a label without going through that service is not covered either.

Part of the stack for #628135 (INC-12449), targeting 628135-04-cascade-delete-on-label-delete. Merge in this order. The numbers follow the branch names, so there is no MR 2: that change was folded into MR 3 and !255083 (closed) is closed.

  • MR 1 (!255082 (merged)): Render group level work item label notes against the group - merged 2026-09-21
  • MR 3 (!255091 (merged)): Store the label event reference when the event is written - merged 2026-10-07
  • MR 4 (!255092): Delete label events when their label is deleted
    • MR 4c (!257979): Validate the cascade foreign key, %19.6, branches off MR 4 and merges after MR 4 is deployed
  • MR 4b (!255238): Record deleted label events in the label audit event <-- this MR
  • MR 5 (!255093): Link a project label in a group context to its project
  • MR 6 (!255096): Backfill the reference on existing label events
  • MR 7 (!255097): Stop writing the label event reference on a read request

Screenshots or screen recordings

Not applicable, backend only.

How to set up and validate locally

Run the specs:

bundle exec rspec spec/models/concerns/batch_delete_dependent_associations_spec.rb ee/spec/services/ee/labels/destroy_service_spec.rb

13 examples, 0 failures locally.

In the UI, on a GDK with an EE license:

  1. Create a project label, apply it to an issue, then remove it, so there are two timeline entries.
  2. Delete the label.
  3. Open the project's audit events page and read the newest entry.

In a rails console (gdk rails console), wrapped in a transaction that rolls back, so it leaves nothing behind:

ActiveRecord::Base.transaction do
  user   = User.find_by_username('root')
  org    = user.organizations.first
  suffix = SecureRandom.hex(4)

  group = Groups::CreateService.new(user, name: "repro-#{suffix}", path: "repro-#{suffix}",
                                    organization_id: org.id).execute[:group]
  group.add_owner(user)
  project = Projects::CreateService.new(user, name: "proj-#{suffix}", path: "proj-#{suffix}",
                                        namespace_id: group.id, organization_id: org.id).execute
  issue = project.issues.create!(author: user, title: 'repro 628135')
  label = Labels::CreateService.new(user, title: 'doomed').execute(project: project)

  ResourceEvents::ChangeLabelsService.new(issue, user).execute(added_labels: [label])
  ResourceEvents::ChangeLabelsService.new(issue, user).execute(removed_labels: [label])

  Labels::DestroyService.new(user, label).execute
  puts "tally: #{label.deleted_dependent_association_counts[:resource_label_events].inspect}"

  audit = AuditEvents::ProjectAuditEvent.order(:id).last
  if audit
    puts "message: #{audit.details[:custom_message].inspect}"
    puts "count:   #{audit.details[:resource_label_events_deleted].inspect}"
  else
    puts "no audit event written (this instance has no EE license)"
  end

  raise ActiveRecord::Rollback
end

Observed output

The script above was run on the parent branch and on this branch; what follows is the actual output from both runs.

Before, on 628135-04-cascade-delete-on-label-delete:

/home/gdk/.local/share/mise/installs/ruby/3.3.11/lib/ruby/gems/3.3.0/gems/activemodel-7.2.3.1/lib/active_model/attribute_methods.rb:512:in `method_missing': undefined method `deleted_dependent_association_counts' for an instance of ProjectLabel (NoMethodError)
Did you mean?  delete_dependent_associations_in_batches

After, on 628135-04b-audit-label-event-deletion:

tally: 2
message: "Deleted label doomed and 2 timeline entries recording its use"
count:   2

The project's Audit events page after the label is deleted:

Before, on 628135-04-cascade-delete-on-label-delete After, on 628135-04b-audit-label-event-deletion
mr04b-before-audit-events mr04b-after-audit-events
The row says a label was deleted and nothing about what went with it. The row carries the tally, and resource_label_events_deleted: 2 is in the details.

This MR previously noted that the audit event could not be observed by hand, because the local GDK has no EE license, so the audit message rested on the EE spec alone. That gap is now closed. A self-signed Ultimate license was installed in the disposable box, and the before and after audit rows were read off the project's Audit events page as real records rather than inferred from the specs.

Both runs were done on a disposable GDK box whose GDK was at master 8d8af6729f, with each branch applied over it as a runtime-only patch and migrations run for real where the branch adds them.

MR acceptance checklist

This MR is a draft in a stacked chain; the checklist applies to the chain as a whole.

Edited by Alexandru Croitor

Merge request reports

Loading
Loading