Skip to content

[COMMS-1067] Journal admin deletion of a label - #26191

Open
akabiru wants to merge 17 commits into
devfrom
implementation/comms-1067-journal-admin-label-deletion
Open

akabiru wants to merge 17 commits into
devfrom
implementation/comms-1067-journal-admin-label-deletion

Conversation

@akabiru

@akabiru akabiru commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

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

Screenshot 2026-10-30 at 5 49 36 PM

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 on CauseOfChange::Base

 cause: {"type" => "label_deleted", "label_ids" => [74, 91], "label_names" => ["chore", "release"]},
sequenceDiagram
  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 entry
Loading

The 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

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

@akabiru akabiru self-assigned this Oct 21, 2026
@akabiru
akabiru added this pull request to stack #26192 October 21, 2026 08:19
@github-actions github-actions Bot added ai: Directed 🪄 A human specified the requirements and AI implemented most of it; They validated via testing. ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. and removed ai: Directed 🪄 A human specified the requirements and AI implemented most of it; They validated via testing. labels Oct 21, 2026
@akabiru
akabiru force-pushed the implementation/comms-1067-journal-admin-label-deletion branch 3 times, most recently from eba7da9 to 0a2ff71 Compare October 21, 2026 12:39
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./modules/bim/spec/features/viewer/create_viewpoint_spec.rb[1:2:1:1]
🤖 Ask Copilot to investigate

Copy 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.

@copilot The following spec(s) are flaky in CI (first seen on PR #26191, linked for reference only):

- `rspec ./modules/bim/spec/features/viewer/create_viewpoint_spec.rb[1:2:1:1]`

Treat this as a standalone task, unrelated to PR #26191. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #26191 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @akabiru to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @akabiru, and request a review from @akabiru.
On every commit, set @akabiru as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

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
akabiru force-pushed the implementation/comms-1067-journal-admin-label-deletion branch from 93a4f70 to f88a01a Compare October 21, 2026 14:10
Base automatically changed from implementation/comms-1022-labels-journaling to dev October 21, 2026 15:05
@akabiru
akabiru removed this pull request from stack #26192 October 28, 2026 12:21
akabiru and others added 3 commits October 28, 2026 15:21
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
akabiru force-pushed the implementation/comms-1067-journal-admin-label-deletion branch from b2edcf4 to dc8e045 Compare October 30, 2026 09:27
@akabiru
akabiru added this pull request to stack #26490 October 30, 2026 13:16
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.
@akabiru
akabiru removed this pull request from stack #26490 October 30, 2026 14:52
Reading a nil return from merge_into as "cannot merge" lives in one
predicate, so the aggregation check and the merge agree on it.
@akabiru
akabiru marked this pull request as ready for review October 30, 2026 15:29
@akabiru
akabiru requested review from a team and ulferts October 30, 2026 15:29
@akabiru akabiru added this to the 18.0.x milestone Oct 30, 2026
@akabiru
akabiru requested a balanced review from Copilot November 1, 2026 09:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line.

Development

Successfully merging this pull request may close these issues.

2 participants