Store the label event reference when the event is written
This adds the entry-point writes that the rest of the stack depends on. Instead of leaving resource_label_events.reference to be filled in and saved the first time a note renders, compute and store it at the points that create or move the row:
ResourceEvents::ChangeLabelsServiceresolves the reachable labels once for the batch and puts the reference straight into the bulk insert. The resolving itself moves into a new service,Issuables::LabelEventReferencesService, which takes the parent and a set of labels and returns a reference per label id; it wraps theResourceLabelEvent.reference_for(label, parent, local_label_ids)class method this MR adds, plus theLabelsFinderlookup that decides which labels are local. Both entry points below call that same service, so the two paths cannot drift apart.Issuable::Clone::CopyResourceEventsServicerecomputes the reference against the new parent when copying events to a cloned issuable, rather than copying a value computed for the old parent that may not resolve there.Labels::PromoteServicerepoints the reference at the group label in the same batched update that already repointslabel_id, splitting it so that events on issuables outside the group get the qualified form. This also fixes a standing bug on its own: promoting a project label to a group label left every event pointing at the destroyed project label, and the note rendered that dead reference forever.- A
before_validation ... on: :createguard onResourceLabelEventcovers paths that create one row at a time. It skips rows written during an import (unless: :importing?), so the GitHub importer and project import leave the reference NULL, as they do today; !259406 fills those rows once the import finishes. Bulk inserts skip callbacks, so those paths set the value themselves inline.
The read path is untouched here. Removing the lazy write happens later in the stack (MR 7), after the entry points cover new rows and the backfill (MR 6) covers old ones.
Part of the stack for #628135 (INC-12449), targeting master now that MR 1 has merged. 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 <-- this MR
- MR 4 (!255092): Delete label events when their label is deleted
- MR 4b (!255238): Record deleted label events in the label audit event
- 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
Database
See the database review guidelines.
Plans below are EXPLAIN (ANALYZE, BUFFERS) from a Database Lab thin clone (gitlab.com production-sized data, via pgai). I ran each one twice and kept the warm plan, noting cold timings separately. Queries come from bin/rails runner ... to_sql on this branch, with gitlab.com ids substituted in. Long id lists are shortened to ... in the plans. resource_label_events has about 672M rows, labels about 42M.
New queries
This MR adds one LabelsFinder lookup per write in ChangeLabelsService and CopyResourceEventsService (reachable labels, id_in(...) plus pluck_primary_key), and one Label load in the copy path.
How large can the IN list get
The IN list holds only the labels being written in that request, never every label in the namespace. The plucked set is a subset of it, and Postgres probes labels_pkey per id.
In ChangeLabelsService, the list is the labels added or removed in one update. The only cap is Issue::MAX_NUMBER_OF_LABELS = 100, and that sits behind the default-off limit_labels_per_work_item flag and only applies to issues. Otherwise the list is bounded by the labels a user can apply at all, about 5.7k for gitlab-org/gitlab (1464 project labels plus 4212 group labels). The same request already inserts that many label events, so this is not a new order of magnitude.
In CopyResourceEventsService, the list is the distinct labels ever applied to the source issue. Across all of gitlab-org/gitlab: p50 is 11, p99 is 30, max is 91 (issue id 24698079). Across the most recent 20M events globally, the max is 67. I planned at 500 ids for headroom: 1.4 ms warm, 176 ms cold.
Every plan is index-backed and under 10 ms warm at realistic sizes; the finder lookups are labels_pkey scans under 2 ms.
ChangeLabelsService
Reachable labels, project parent (Issuables::LabelEventReferencesService#local_label_ids_for, reached from ResourceEvents::ChangeLabelsService). Project gitlab-org/gitlab (278964), 5 project labels plus 5 labels of group gitlab-org (9970):
SELECT "labels"."id" FROM "labels"
WHERE ("labels"."type" = 'GroupLabel' AND "labels"."group_id" IN (9970)
OR "labels"."type" = 'ProjectLabel' AND "labels"."project_id" = 278964)
AND "labels"."id" IN (53934305, 53932318, 53932313, 53926719, 53923408,
53902837, 53891105, 53888137, 53888075, 53888069);Plan: 10 ids, 0.08 ms warm, 13.7 ms cold
Index Scan using labels_pkey on public.labels (cost=0.44..23.72 rows=1 width=4) (actual time=0.025..0.053 rows=10 loops=1)
Output: id
Index Cond: (labels.id = ANY ('{53934305,...,53888069}'::integer[]))
Filter: ((((labels.type)::text = 'GroupLabel'::text) AND (labels.group_id = 9970)) OR (((labels.type)::text = 'ProjectLabel'::text) AND (labels.project_id = 278964)))
Buffers: shared hit=31
Planning Time: 0.170 ms
Execution Time: 0.076 msPlan: 50 ids, 0.29 ms warm, 67 ms cold
Index Scan using labels_pkey on public.labels (cost=0.44..113.75 rows=1 width=4) (actual time=0.031..0.259 rows=50 loops=1)
Output: id
Index Cond: (labels.id = ANY ('{149397,354884,...,2534166}'::integer[]))
Filter: ((((labels.type)::text = 'GroupLabel'::text) AND (labels.group_id = 9970)) OR (((labels.type)::text = 'ProjectLabel'::text) AND (labels.project_id = 278964)))
Buffers: shared hit=170
Planning Time: 0.179 ms
Execution Time: 0.289 msPlan: project 47441940, nine levels deep under gitlab-org, 10 ids, 0.07 ms warm, 2.9 ms cold
Index Scan using labels_pkey on public.labels (cost=0.46..23.77 rows=1 width=4) (actual time=0.023..0.050 rows=10 loops=1)
Output: id
Index Cond: (labels.id = ANY ('{53902837,...,53780937}'::integer[]))
Filter: ((((labels.type)::text = 'GroupLabel'::text) AND (labels.group_id = ANY ('{9970,11787569,11805471,14491403,61550135,69731974,69732123,69732124,69732127}'::integer[]))) OR (((labels.type)::text = 'ProjectLabel'::text) AND (labels.project_id = 47441940)))
Buffers: shared hit=25
Planning Time: 0.145 ms
Execution Time: 0.069 msReachable labels, group parent (group level work item on gitlab-org, only_group_labels: true). The second UNION branch is existing LabelsFinder behaviour:
SELECT "labels"."id" FROM (
(SELECT "labels".* FROM "labels" WHERE "labels"."group_id" = 9970)
UNION
(SELECT "labels".* FROM "labels" WHERE "labels"."group_id" IN (
SELECT "projects"."namespace_id" FROM "projects"
INNER JOIN "namespaces" ON "namespaces"."id" = "projects"."namespace_id"
WHERE "projects"."namespace_id" = 9970 AND "namespaces"."type" = 'Group'))
) labels
WHERE "labels"."id" IN (53934305, 53932318, 53932313, 53926719, 53923408,
53902837, 53891105, 53888137, 53888075, 53888069);Plan: 10 ids, 0.19 ms warm, 6.1 ms cold
The remaining Sort feeds Unique, which is how Postgres removes UNION duplicates. It is not an ordering of the result.
Subquery Scan on labels (cost=55.43..55.53 rows=2 width=4) (actual time=0.148..0.156 rows=5 loops=1)
Buffers: shared hit=113
-> Unique (cost=55.43..55.51 rows=2 width=1171) (actual time=0.147..0.155 rows=5 loops=1)
Buffers: shared hit=113
-> Sort (cost=55.43..55.43 rows=2 width=1171) (actual time=0.146..0.148 rows=10 loops=1)
Sort Key: labels_1.id, labels_1.title, ... (every labels column)
Sort Method: quicksort Memory: 27kB
Buffers: shared hit=113
-> Append (cost=0.44..55.42 rows=2 width=1171) (actual time=0.021..0.118 rows=10 loops=1)
Buffers: shared hit=113
-> Index Scan using labels_pkey on public.labels labels_1 (cost=0.44..23.65 rows=1 width=206) (actual time=0.020..0.044 rows=5 loops=1)
Index Cond: (labels_1.id = ANY ('{53934305,...,53888069}'::integer[]))
Filter: (labels_1.group_id = 9970)
Rows Removed by Filter: 5
Buffers: shared hit=31
-> Nested Loop Semi Join (cost=1.57..31.76 rows=1 width=206) (actual time=0.032..0.071 rows=5 loops=1)
Buffers: shared hit=82
-> Index Scan using labels_pkey on public.labels labels_2 (cost=0.44..23.65 rows=1 width=206) (actual time=0.007..0.017 rows=5 loops=1)
Index Cond: (labels_2.id = ANY ('{53934305,...,53888069}'::integer[]))
Filter: (labels_2.group_id = 9970)
Rows Removed by Filter: 5
Buffers: shared hit=31
-> Nested Loop (cost=1.13..8.09 rows=34 width=4) (actual time=0.010..0.010 rows=1 loops=5)
Buffers: shared hit=51
-> Index Scan using namespaces_pkey on public.namespaces (cost=0.57..3.59 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=5)
Index Cond: (namespaces.id = 9970)
Filter: ((namespaces.type)::text = 'Group'::text)
Buffers: shared hit=25
-> Index Only Scan using index_projects_on_namespace_id_and_id on public.projects (cost=0.56..4.16 rows=34 width=4) (actual time=0.007..0.007 rows=1 loops=5)
Index Cond: (projects.namespace_id = 9970)
Heap Fetches: 5
Buffers: shared hit=26
Planning Time: 0.574 ms
Execution Time: 0.191 msPlan: 50 ids, 0.64 ms
Subquery Scan on labels (cost=234.88..234.98 rows=2 width=4) (actual time=0.572..0.600 rows=30 loops=1)
Buffers: shared hit=641
-> Unique -> Sort (UNION dedup, every labels column) -> Append (actual time=0.034..0.494 rows=60 loops=1)
-> Index Scan using labels_pkey on public.labels labels_1 (actual time=0.033..0.193 rows=30 loops=1)
Index Cond: (labels_1.id = ANY ('{149397,...,2534166}'::integer[]))
Filter: (labels_1.group_id = 9970)
Rows Removed by Filter: 20
Buffers: shared hit=170
-> Nested Loop Semi Join (actual time=0.042..0.294 rows=30 loops=1)
Buffers: shared hit=471
-> Index Scan using labels_pkey on public.labels labels_2 (actual time=0.011..0.068 rows=30 loops=1)
-> Nested Loop (actual time=0.007..0.007 rows=1 loops=30)
-> Index Scan using namespaces_pkey (namespaces.id = 9970) loops=30
-> Index Only Scan using index_projects_on_namespace_id_and_id (projects.namespace_id = 9970) loops=30, Heap Fetches: 30
Planning Time: 0.583 ms
Execution Time: 0.640 msCopyResourceEventsService
Labels of the source issue (Gitlab::Issuable::Clone::CopyResourceEventsService#copy_resource_label_events):
SELECT "labels".* FROM "labels"
WHERE "labels"."id" IN (SELECT "resource_label_events"."label_id" FROM "resource_label_events"
WHERE "resource_label_events"."issue_id" = 24698079)
ORDER BY "labels"."title" ASC;Plan: issue 24698079, the most distinct labels in gitlab-org/gitlab (91), 0.78 ms warm, 266 ms cold
Sort (cost=454.08..454.37 rows=119 width=207) (actual time=0.732..0.737 rows=91 loops=1)
Sort Key: labels.title
Buffers: shared hit=533
-> Nested Loop (actual time=0.105..0.603 rows=91 loops=1)
-> HashAggregate (actual time=0.095..0.107 rows=92 loops=1)
Group Key: resource_label_events.label_id
-> Index Only Scan using index_resource_label_events_issue_id_label_id_action on public.resource_label_events (actual time=0.020..0.067 rows=183 loops=1)
Index Cond: (resource_label_events.issue_id = 24698079)
Heap Fetches: 1
Buffers: shared hit=169
-> Index Scan using labels_pkey on public.labels (actual time=0.005..0.005 rows=1 loops=92)
Index Cond: (labels.id = resource_label_events.label_id)
Buffers: shared hit=364
Planning Time: 0.370 ms
Execution Time: 0.780 msPlan: issue 203588034, the most label events found (18,621, 7 labels), 9.3 ms warm, 2.6 s cold
The cold cost is reading the event rows, which the existing find_in_batches copy reads right after anyway.
Sort (cost=73474.63..73526.48 rows=20742 width=207) (actual time=8.874..8.876 rows=7 loops=1)
Buffers: shared hit=8608
-> Nested Loop (actual time=8.720..8.861 rows=7 loops=1)
-> HashAggregate (actual time=8.706..8.824 rows=7 loops=1)
-> Index Only Scan using index_resource_label_events_issue_id_label_id_action on public.resource_label_events (actual time=0.012..6.892 rows=18621 loops=1)
Index Cond: (resource_label_events.issue_id = 203588034)
Heap Fetches: 8401
Buffers: shared hit=8580
-> Index Scan using labels_pkey on public.labels (actual time=0.004..0.004 rows=1 loops=7)
Planning Time: 0.381 ms
Execution Time: 9.290 msReachable labels in the target, project parent. Issue 24698079 moved to gitlab-com/www-gitlab-com (project 7764, group 6543):
SELECT "labels"."id" FROM "labels"
WHERE ("labels"."type" = 'GroupLabel' AND "labels"."group_id" IN (6543)
OR "labels"."type" = 'ProjectLabel' AND "labels"."project_id" = 7764)
AND "labels"."id" IN (10690780, 10690781, ... 91 ids);Plan: 91 ids, 0.40 ms warm, 44 ms cold
Index Scan using labels_pkey on public.labels (cost=0.44..209.28 rows=1 width=4) (actual time=0.064..0.375 rows=1 loops=1)
Output: id
Index Cond: (labels.id = ANY ('{10690780,10690781,...}'::integer[]))
Filter: ((((labels.type)::text = 'GroupLabel'::text) AND (labels.group_id = 6543)) OR (((labels.type)::text = 'ProjectLabel'::text) AND (labels.project_id = 7764)))
Rows Removed by Filter: 90
Buffers: shared hit=343
Planning Time: 0.176 ms
Execution Time: 0.395 msSame plan shape at other sizes: 91 ids within gitlab-org/gitlab 0.43 ms, 500 ids 1.4 ms warm and 176 ms cold.
Reachable labels in the target, group parent (group gitlab-org, 9970):
SELECT "labels"."id" FROM (
(SELECT "labels".* FROM "labels" WHERE "labels"."group_id" IN (9970))
UNION
(SELECT "labels".* FROM "labels" WHERE "labels"."group_id" IN (
SELECT "projects"."namespace_id" FROM "projects"
INNER JOIN "namespaces" ON "namespaces"."id" = "projects"."namespace_id"
WHERE "projects"."namespace_id" = 9970 AND "namespaces"."type" = 'Group'))
) labels
WHERE "labels"."id" IN (10690780, ... 91 ids);Plan: 91 ids, 1.4 ms
Subquery Scan on labels (cost=425.32..425.42 rows=2 width=4) (actual time=1.313..1.381 rows=88 loops=1)
Buffers: shared hit=1567
-> Unique -> Sort (UNION dedup, every labels column) -> Append (actual time=0.024..1.140 rows=176 loops=1)
-> Index Scan using labels_pkey on public.labels labels_1 (actual time=0.023..0.359 rows=88 loops=1)
Index Cond: (labels_1.id = ANY ('{10690780,10690781,...}')) Filter: (labels_1.group_id = 9970)
Rows Removed by Filter: 3
-> Nested Loop Semi Join (actual time=0.049..0.765 rows=88 loops=1)
-> Index Scan using labels_pkey on public.labels labels_2 (rows=88)
-> Nested Loop (loops=88)
-> Index Scan using namespaces_pkey (Index Cond: id = 9970)
-> Index Only Scan using index_projects_on_namespace_id_and_id (Index Cond: namespace_id = 9970)
Planning Time: 0.590 ms
Execution Time: 1.425 msFor subgroup 1755573 with 500 ids the planner switches to a BitmapAnd of index_labels_on_group_id and labels_pkey: 2.6 ms warm, 9.5 ms cold.
Label promotion
Master already runs one unbatched update_all over every event of the promoted labels. This MR sets reference too and splits the update in two by namespace, but touches the same rows.
After the first statement only events outside the group still match the label ids, so the second statement needs no namespace filter. self_and_descendants(skope: Namespace) compiles to traversal_ids @>, served by the GIN index.
Cost against master, on a real 120,356-row label in gitlab-org/gitlab: 3.4 s master warm vs 3.7 s branch warm, and the second statement then finds 0 rows in 54 ms.
Measured on label 28392176 in gitlab-org/gitlab, 120,356 events, promoted to a group label in gitlab-org. Every statement was wrapped in BEGIN; ... ROLLBACK;.
Before (master):
UPDATE "resource_label_events" SET "label_id" = 992791
WHERE "resource_label_events"."label_id" IN (28392176);Plan: 3.4 s warm, 19.4 s cold
Update on resource_label_events (cost=0.57..260961.65 rows=0 width=0) (actual time=2668.846..2668.847 rows=0 loops=1)
Buffers: shared hit=4093845 read=1134 dirtied=1362 written=628
I/O Timings: shared read=408.255 write=11.520
-> Index Scan using index_resource_label_events_on_label_id_and_action on resource_label_events (cost=0.57..260961.65 rows=172492 width=10) (actual time=0.017..75.749 rows=120356 loops=1)
Index Cond: (label_id = 28392176)
Buffers: shared hit=61584 dirtied=21
Planning Time: 0.086 ms
Trigger for constraint fk_rails_b126799f57: time=697.601 calls=120356
Execution Time: 3375.732 msAfter, events inside the group:
UPDATE "resource_label_events" SET "label_id" = 992791, "reference" = '~992791'
WHERE "resource_label_events"."label_id" IN (28392176)
AND "resource_label_events"."namespace_id" IN (
SELECT "namespaces"."id" FROM "namespaces" WHERE (traversal_ids @> ('{9970}')));Plan: 3.7 s warm and cold (the clone was already warm from the master run)
Update on resource_label_events (cost=16769.65..278183.52 rows=0 width=0) (actual time=2952.276..2952.279 rows=0 loops=1)
Buffers: shared hit=4170164 read=1090 dirtied=1587 written=894
I/O Timings: shared read=312.484 write=16.665
-> Hash Join (cost=16769.65..278183.52 rows=18 width=48) (actual time=16.090..124.824 rows=120356 loops=1)
Hash Cond: (resource_label_events.namespace_id = namespaces.id)
Buffers: shared hit=71237
-> Index Scan using index_resource_label_events_on_label_id_and_action on resource_label_events (cost=0.57..260961.65 rows=172492 width=14) (actual time=0.014..79.029 rows=120356 loops=1)
Index Cond: (label_id = 28392176)
Buffers: shared hit=61584
-> Hash (cost=16647.98..16647.98 rows=9688 width=10) (actual time=16.058..16.059 rows=10605 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 584kB
Buffers: shared hit=9653
-> Bitmap Heap Scan on namespaces (cost=358.62..16647.98 rows=9688 width=10) (actual time=4.522..14.635 rows=10605 loops=1)
Recheck Cond: (traversal_ids @> '{9970}'::integer[])
Heap Blocks: exact=9477
Buffers: shared hit=9653
-> Bitmap Index Scan on index_namespaces_on_traversal_ids (cost=0.00..356.20 rows=9688 width=0) (actual time=3.207..3.208 rows=10605 loops=1)
Index Cond: (traversal_ids @> '{9970}'::integer[])
Buffers: shared hit=176
Planning Time: 0.524 ms
Trigger for constraint fk_rails_b126799f57: time=693.848 calls=120356
Execution Time: 3654.945 msAfter, events outside the group. Run after the first statement in the same transaction, as in the service:
UPDATE "resource_label_events" SET "label_id" = 992791, "reference" = 'gitlab-org~992791'
WHERE "resource_label_events"."label_id" IN (28392176);Plan: 54 ms, 0 rows left to update
Update on resource_label_events (cost=0.57..260961.65 rows=0 width=0) (actual time=53.885..53.886 rows=0 loops=1)
Buffers: shared hit=61511 read=73
I/O Timings: shared read=1.196
-> Index Scan using index_resource_label_events_on_label_id_and_action on resource_label_events (cost=0.57..260961.65 rows=172492 width=42) (actual time=53.883..53.883 rows=0 loops=1)
Index Cond: (label_id = 28392176)
Buffers: shared hit=61511 read=73
I/O Timings: shared read=1.196
Planning Time: 0.108 ms
Execution Time: 53.921 msFor a small label (label 23984398, 9 events) the first statement takes 2.5 ms warm and 28.7 ms cold, against 0.6 ms and 73 ms on master.
Points from the review
reference_forloaded each non-local label's container and route one at a time, so 2 queries per label. This hit cross-group moves: work item data sync passes up to 100 labels per batch (up to about 200 queries), and the copy service (91 labels on the worst issue I found). Master does not issue these queries at all. Fixed:ResourceLabelEvent.preload_reference_containerspreloadsgroup: :routefor group labels andproject: [:route, { namespace: :route }]for project labels, called fromIssuables::LabelEventReferencesService#executefor the labels outside the parent, which is the one place bothChangeLabelsServiceandCopyResourceEventsServicego through. Two query-count specs cover it, one per service, each comparing 2 labels against 6 labels outside the parent. Before the fix: "Expected a maximum of 8 queries, got 16" (ChangeLabelsService) and "Expected a maximum of 17 queries, got 25" (CopyResourceEventsService). Both pass now.- Promotion's event update is not batched by row. That is pre-existing, not something this MR adds. The largest case I found, the title
issuein one customer group at 3.7M rows, hits the 15s statement timeout on master and on this branch alike, inside the promote transaction on a web request. Fixing it is a follow-up. - Also fixed: the second promote UPDATE dropped its NOT IN filter. After the first statement moves the in-group rows, only events outside the group still match the label ids, so the filter was redundant.
For reference: the bumped query thresholds in two request specs (+2) come from the finder's ancestor-group load plus the pluck, not from route lookups.
Requesting a database review.
How to reproduce and check by hand
Two things to check. The reference is now stored when the row is created, not the first time someone opens the page. And promoting a project label to a group label used to leave the stored reference pointing at the project label that the promotion destroys, so the note rendered a dead reference from then on.
In the UI:
- Create a project inside a group, create a project label, and apply it to an issue.
- Open the issue once, so the note renders.
- Promote the label to group level from the project label list, then reload the issue.
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: 'bug').execute(project: project)
notes = ->(issuable) do
ResourceEvents::SyntheticLabelNotesBuilderService.new(issuable.reset, user).execute.map(&:note).join(' | ')
end
ResourceEvents::ChangeLabelsService.new(issue, user).execute(added_labels: [label])
event = issue.resource_label_events.last
puts "1. reference at creation: #{event.reference.inspect}"
# an event copied onto an issue in another group, as a move does
other_group = Groups::CreateService.new(user, name: "other-#{suffix}", path: "other-#{suffix}",
organization_id: org.id).execute[:group]
other_group.add_owner(user)
other_project = Projects::CreateService.new(user, name: "other-#{suffix}", path: "other-#{suffix}",
namespace_id: other_group.id, organization_id: org.id).execute
other_issue = other_project.issues.create!(author: user, title: 'moved copy')
copied = ResourceLabelEvent.create!(issue: other_issue, label: label, user: user, action: 'add')
notes.call(issue) && notes.call(other_issue) # first render, as a visitor would trigger
puts "2. reference after first render: #{event.reload.reference.inspect}"
puts " project label id: #{label.id}"
Labels::PromoteService.new(project, user).execute(label)
puts "3. group label id: #{group.labels.find_by(title: 'bug').id}"
puts "4. reference, same group: #{event.reload.reference.inspect}"
puts "5. reference, other group: #{copied.reload.reference.inspect}"
puts "6. note, same group: #{notes.call(issue)}"
puts "7. note, other group: #{notes.call(other_issue)}"
puts " project label still exists? #{Label.exists?(label.id)}"
raise ActiveRecord::Rollback
endObserved 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-01-work-item-note-parent (MR 1, now merged into master):
1. reference at creation: nil
2. reference after first render: "~8"
project label id: 8
3. group label id: 9
4. reference, same group: "~8"
5. reference, other group: "repro-5b7a1327/proj-5b7a1327~8"
6. note, same group: added ~8 label
7. note, other group: added repro-5b7a1327/proj-5b7a1327~8 label
project label still exists? falseAfter, on 628135-03-write-reference-at-entry-points:
1. reference at creation: "~15"
2. reference after first render: "~15"
project label id: 15
3. group label id: 16
4. reference, same group: "~16"
5. reference, other group: "repro-830babb9~16"
6. note, same group: added ~16 label
7. note, other group: added repro-830babb9~16 label
project label still exists? falseThe issue activity feed after the promotion:
Line 1 is the change itself: the reference is stored when the row is written, not left for the first render to fill in. Lines 4 and 5 are the promotion: on the parent branch both still name label 8, which the promotion destroyed, so both notes render a dead reference. On this branch the promotion repoints them, and the copy sitting on an issue in another group gets the qualified repro-830babb9~16 because a bare id does not resolve from outside the group.
After the label is promoted away, the timeline entry does not render at all in the work item activity feed on the parent branch. On this branch it renders as a working label chip.
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.
Testing
New examples in change_labels_service_spec, promote_service_spec, copy_resource_events_service_spec, and resource_label_event_spec all pass, along with a new spec/services/issuables/label_event_references_service_spec.rb covering the extracted service directly. Rubocop is clean on every changed file.
Related to #628135

