Repository navigation
Conversation
akabiru
force-pushed
the
implementation/comms-1067-journal-admin-label-deletion
branch
3 times, most recently
from
October 21, 2026 12:39
eba7da9 to
0a2ff71
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
The label row is gone when the journals are written, so the cause carries the name alongside the id. See COMMS-1067.
Writes one journal per affected work package, authored by the admin, with the label name snapshotted into the cause since the label row is gone. The journals are written directly, so no notifications go out for them. See COMMS-1067.
The delete service collects the labeled work packages before the label goes and hands them to the journaling job once the deletion is committed. See COMMS-1067.
The XSS example only asserted the HTML variant; raw text must keep the name verbatim.
The guard subquery was re-run for every batch and could only scan the whole journals table; one indexed lookup over the given work packages replaces it.
The epic defines the dialog text without a note about journaling.
A cause may now declare that it extends the predecessor's cause; the aggregated journal then carries the merged cause. Deleting several labels in a row leaves one entry per work package listing all of them. See COMMS-1067.
Lists every label of an aggregated deletion and pluralises the verb. See COMMS-1067.
Keeps the journal service free of duck-typing checks.
The cause line already names the removed labels. See COMMS-1067.
Use the store accessor for the cause type and extract the predecessor cause aggregation into a named method.
akabiru
force-pushed
the
implementation/comms-1067-journal-admin-label-deletion
branch
from
October 21, 2026 14:10
93a4f70 to
f88a01a
Compare
Base automatically changed from
implementation/comms-1022-labels-journaling
to
dev
October 21, 2026 15:05
Hide the labels change on deletion entries in the formatter so every surface (activities tab, project activity, API) renders only the cause. Check the re-run guard per work package inside the advisory lock, since GoodJob is at-least-once and the bulk pre-query scanned unindexed jsonb. Replace the two-method cause aggregation protocol with a single merge_into hook.
akabiru
force-pushed
the
implementation/comms-1067-journal-admin-label-deletion
branch
from
October 30, 2026 09:27
b2edcf4 to
dc8e045
Compare
1 of 3 tasks
The readonly-attribute cause was the one plain Hash reaching the service, so cause merging had to guard with try. With every cause a CauseOfChange::Base, the service calls merge_into directly and an untyped cause fails loudly.
Reading a nil return from merge_into as "cannot merge" lives in one predicate, so the aggregation check and the merge agree on it.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical race conditions can misattribute label removals or omit deletion causes, while direct journal updates can lose completion events.
4 open issues
What changed in this PR
Extends work-package label history from #25320 to record admin label deletions, including the deleting admin and deleted label names.
Changes:
- Adds a background job to journal deletions on affected work packages.
- Introduces mergeable causes for consecutive label deletions and suppresses redundant label details.
- Adds coverage for deletion, aggregation, formatting, and activity rendering.
| File | Description |
|---|---|
spec/workers/labels/journalize_deletion_job_spec.rb |
Tests authorship, retry deduplication, and aggregation. |
spec/services/work_packages/set_attributes_service_spec.rb |
Tests typed causes for read-only attribute writes. |
spec/services/labels/delete_service_spec.rb |
Tests deletion job enqueue conditions. |
spec/models/work_package/work_package_acts_as_journalized_spec.rb |
Tests cause objects and deletion aggregation. |
spec/lib/open_project/journal_formatter/labels_spec.rb |
Tests suppression of explained removals. |
spec/lib/open_project/journal_formatter/cause_spec.rb |
Tests deletion messages, pluralization, and escaping. |
spec/lib/api/v3/activities/activity_representer_rendering_spec.rb |
Tests omission of redundant API details. |
spec/features/admin/labels/manage_spec.rb |
Checks that admin deletion enqueues journalization. |
spec/components/work_packages/activities_tab/journals/item_component/details_spec.rb |
Tests cause-only activity rendering. |
lib/open_project/journal_formatter/labels.rb |
Suppresses removals explained by deletion causes. |
lib/open_project/journal_formatter/cause.rb |
Formats deleted label names. |
lib_static/plugins/acts_as_journalized/lib/cause_of_change/base.rb |
Adds cause-merging hooks. |
config/locales/en.yml |
Adds label-deletion messages. |
app/workers/labels/journalize_deletion_job.rb |
Journals deletions with retry deduplication. |
app/services/work_packages/set_attributes_service.rb |
Uses a typed read-only attribute cause. |
app/services/labels/delete_service.rb |
Collects affected work packages and enqueues journalization. |
app/services/journals/create_service.rb |
Supports aggregation of compatible causes. |
app/models/journal/caused_by_label_deletion.rb |
Defines and merges label-deletion causes. |
app/models/journal/caused_by_default_attribute_written.rb |
Defines the read-only attribute cause object. |
app/models/journal.rb |
Registers deletion causes and label metadata. |
Review effort: Balanced
| protected | ||
|
|
||
| def persist(service_result) | ||
| ids = labeled_work_package_ids |
| OpenProject::Mutex.with_advisory_lock_transaction(work_package) do | ||
| next if journalized?(work_package, label_id) | ||
|
|
||
| Journals::CreateService.new(work_package, user).call(cause:) |
| OpenProject::Mutex.with_advisory_lock_transaction(work_package) do | ||
| next if journalized?(work_package, label_id) | ||
|
|
||
| Journals::CreateService.new(work_package, user).call(cause:) |
| old_ids, new_ids = values.map { |value| value.to_s.split(",").map(&:to_i) } | ||
| added = new_ids - old_ids | ||
| removed = old_ids - new_ids | ||
| added.empty? && removed.to_set == Array(@journal.cause_label_ids).to_set(&:to_i) |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Ticket
https://community.openproject.org/wp/COMMS-1067
What are you trying to accomplish?
Deleting a label in Administration > Labels removed it from every work package without a clear trace. Each affected work package now gets an activity entry authored by the deleting admin, "Label removed: 'Frontend' was deleted", or "Label removed: 'Frontend', 'Backend' were deleted" when the same admin deletes several labels within the journal aggregation window.
The entry carries no "Labels changed" line of its own; the removed labels are named in the cause.
Screenshots
What approach did you choose and why?
Note
This PR introduces a new concept to journaling: aggregating causes via a
merge_into(predecessor_cause), nil by default onCauseOfChange::BasesequenceDiagram participant A as Admin in the labels administration participant C as Admin::LabelsController (destroy) participant S as Labels::DeleteService participant J as Labels::JournalizeDeletionJob participant CS as Journals::CreateService participant T as Activity tab A->>C: DELETE /admin/labels/:id C->>S: call S->>S: pluck labeled work package ids S->>S: destroy label (labelings cascade) S->>J: perform_later(work_package_ids, label_id, label_name, user_id) S-->>C: success C-->>A: redirect, flash "Successful deletion." J->>J: skip work packages whose journals already carry this label id loop each remaining work package, under its mutex J->>CS: call(cause: CausedByLabelDeletion(label_ids, label_names)) alt same admin deleted another label within the aggregation window CS->>CS: aggregate into the previous entry, merge the label lists else CS->>CS: new journal with the cause and the current label snapshot end end A->>T: open an affected work package T->>T: render "Label removed: 'A', 'B' were deleted" Note over T: the "Labels changed" detail is hidden on this entryThe labels formatter renders nothing for an entry whose only change is the removal of the deleted labels, so the activities tab, project activity page and API all show just the cause line. If the same admin also edited labels by hand inside the window, that change still renders the entry and is hidden with it, which is acceptable for an admin action.
AI involvement
Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.
Merge checklist