plan_my_stuff 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bb39718bce95afcf03151d7feaed80b192a9d0ae9224ca1cbe1da5936b38cc7
4
- data.tar.gz: 3d82fabbea5dc402278882bd93eedc21d7cbfaf8e868c13f30dfc759a6dc964e
3
+ metadata.gz: 8a3c6c6445f04d677b6d6b4f0219f57326346b3f42aee6839ab98a0bbf680e63
4
+ data.tar.gz: f5f5adb4f7a11e2d35b907ae7ab1e15d9dc47f31313355be7da186bf9c5086d6
5
5
  SHA512:
6
- metadata.gz: d6195d4cd41797ac803b4c3b0fb8a009782f616a2e634f20eb94415f6863337e6ec46705fc0b75af1cd8ff37f07254b7dc3b14e46aff0d3a087bcb01c2adcab3
7
- data.tar.gz: fcc8ed29694e8256132207014bf6f9ceea3fc0f93c9a50cc11601d22cbc745c09e16f058ba243b6069a9662669f3675e6877eaffd97316487ed810ef5a73e1df
6
+ metadata.gz: a31f3331be41dcd8c4a1369f729888e06ec64474a58c0b56d2c7d57d69b647b1f33b7acc0ae4eed26f03491ce5736a3ab60656900fc31e731bbadd2ae68c492c
7
+ data.tar.gz: 429281e1a5b431ab29fc5c2d91f15176c7556533df7a94d9990dc3e98a198d5b2d603205bd29f5d4efd210c131bc67b7d440de296857f2eeddd5c085fbc799a2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Breaking
6
+
7
+ - `ActiveSupport::Notifications` events renamed to follow Rails' convention (event first, library
8
+ last). Subscribers matching the old `plan_my_stuff.*` prefix must switch to `*.plan_my_stuff`.
9
+
10
+ | Old | New |
11
+ |----------------------------------------------|----------------------------------------------|
12
+ | `plan_my_stuff.issue.created` | `issue_created.plan_my_stuff` |
13
+ | `plan_my_stuff.issue.updated` | `issue_updated.plan_my_stuff` |
14
+ | `plan_my_stuff.issue.closed` | `issue_closed.plan_my_stuff` |
15
+ | `plan_my_stuff.issue.reopened` | `issue_reopened.plan_my_stuff` |
16
+ | `plan_my_stuff.issue.archived` | `issue_archived.plan_my_stuff` |
17
+ | `plan_my_stuff.issue.closed_inactive` | `issue_closed_inactive.plan_my_stuff` |
18
+ | `plan_my_stuff.issue.reopened_by_reply` | `issue_reopened_by_reply.plan_my_stuff` |
19
+ | `plan_my_stuff.issue.marked_duplicate` | `issue_marked_duplicate.plan_my_stuff` |
20
+ | `plan_my_stuff.issue.viewers_added` | `issue_viewers_added.plan_my_stuff` |
21
+ | `plan_my_stuff.issue.viewers_removed` | `issue_viewers_removed.plan_my_stuff` |
22
+ | `plan_my_stuff.issue.approval_requested` | `issue_approval_requested.plan_my_stuff` |
23
+ | `plan_my_stuff.issue.approval_granted` | `issue_approval_granted.plan_my_stuff` |
24
+ | `plan_my_stuff.issue.approval_revoked` | `issue_approval_revoked.plan_my_stuff` |
25
+ | `plan_my_stuff.issue.approval_rejected` | `issue_approval_rejected.plan_my_stuff` |
26
+ | `plan_my_stuff.issue.rejection_revoked` | `issue_rejection_revoked.plan_my_stuff` |
27
+ | `plan_my_stuff.issue.all_approved` | `issue_all_approved.plan_my_stuff` |
28
+ | `plan_my_stuff.issue.approvals_invalidated` | `issue_approvals_invalidated.plan_my_stuff` |
29
+ | `plan_my_stuff.issue.reminder_due` | `issue_reminder_due.plan_my_stuff` |
30
+ | `plan_my_stuff.issue.link_reciprocal_failed` | `issue_link_reciprocal_failed.plan_my_stuff` |
31
+ | `plan_my_stuff.comment.created` | `comment_created.plan_my_stuff` |
32
+ | `plan_my_stuff.comment.updated` | `comment_updated.plan_my_stuff` |
33
+ | `plan_my_stuff.label.added` | `label_added.plan_my_stuff` |
34
+ | `plan_my_stuff.label.removed` | `label_removed.plan_my_stuff` |
35
+ | `plan_my_stuff.project_item.added` | `project_item_added.plan_my_stuff` |
36
+ | `plan_my_stuff.project_item.removed` | `project_item_removed.plan_my_stuff` |
37
+ | `plan_my_stuff.project_item.assigned` | `project_item_assigned.plan_my_stuff` |
38
+ | `plan_my_stuff.project_item.status_changed` | `project_item_status_changed.plan_my_stuff` |
39
+ | `plan_my_stuff.pipeline.<status>` | `pipeline_<status>.plan_my_stuff` |
40
+
41
+ Rationale: aligns with `sql.active_record`, `process_action.action_controller`, etc. and lets
42
+ subscribers match "everything from PMS" via the standard suffix-regex (`/\.plan_my_stuff$/`).
43
+
3
44
  ## 0.14.0
4
45
 
5
46
  ### Added
data/README.md CHANGED
@@ -267,31 +267,34 @@ A board and its items are editable through the mounted UI at `/testing_projects`
267
267
 
268
268
  ## Notifications
269
269
 
270
- Every PMS lifecycle write fires an `ActiveSupport::Notifications` event under the `plan_my_stuff.*` namespace. Subscribe to drive email, webhooks, Slack, ActiveJob, etc. Events fire synchronously (no delays) — subscribers that need to defer work should wrap in a background job themselves.
270
+ Every PMS lifecycle write fires an `ActiveSupport::Notifications` event under the `*.plan_my_stuff` namespace
271
+ (Rails convention - event first, library suffix; matches `sql.active_record`, `process_action.action_controller`).
272
+ Subscribe to drive email, webhooks, Slack, ActiveJob, etc. Events fire synchronously (no delays) - subscribers
273
+ that need to defer work should wrap in a background job themselves.
271
274
 
272
275
  ### Event catalog
273
276
 
274
277
  | Event | Fired from |
275
278
  |-------|------------|
276
- | `plan_my_stuff.issue.created` | `Issue.create!` |
277
- | `plan_my_stuff.issue.updated` | `issue.save!` / `issue.update!` (any non-state change) |
278
- | `plan_my_stuff.issue.closed` | `issue.update!(state: :closed)` |
279
- | `plan_my_stuff.issue.reopened` | `issue.update!(state: :open)` |
280
- | `plan_my_stuff.issue.viewers_added` | `issue.add_viewers!` |
281
- | `plan_my_stuff.issue.viewers_removed` | `issue.remove_viewers!` |
282
- | `plan_my_stuff.issue.approval_requested` | `issue.request_approvals!` |
283
- | `plan_my_stuff.issue.approval_granted` | `issue.approve!` |
284
- | `plan_my_stuff.issue.approval_revoked` | `issue.revoke_approval!` |
285
- | `plan_my_stuff.issue.all_approved` | aggregate fires when the set flips to fully approved |
286
- | `plan_my_stuff.issue.approvals_invalidated` | aggregate fires when a revoke (or new approver) drops the set out of fully-approved |
287
- | `plan_my_stuff.comment.created` | `Comment.create!` |
288
- | `plan_my_stuff.comment.updated` | `comment.save!` / `comment.update!` |
289
- | `plan_my_stuff.label.added` | `Label.add!` |
290
- | `plan_my_stuff.label.removed` | `Label.remove!` |
291
- | `plan_my_stuff.project_item.added` | `ProjectItem.create!` (issue or `draft: true`) |
292
- | `plan_my_stuff.project_item.removed` | `project_item.destroy!` |
293
- | `plan_my_stuff.project_item.assigned` | `project_item.assign!` |
294
- | `plan_my_stuff.project_item.status_changed` | `project_item.move_to!` |
279
+ | `issue_created.plan_my_stuff` | `Issue.create!` |
280
+ | `issue_updated.plan_my_stuff` | `issue.save!` / `issue.update!` (any non-state change) |
281
+ | `issue_closed.plan_my_stuff` | `issue.update!(state: :closed)` |
282
+ | `issue_reopened.plan_my_stuff` | `issue.update!(state: :open)` |
283
+ | `issue_viewers_added.plan_my_stuff` | `issue.add_viewers!` |
284
+ | `issue_viewers_removed.plan_my_stuff` | `issue.remove_viewers!` |
285
+ | `issue_approval_requested.plan_my_stuff` | `issue.request_approvals!` |
286
+ | `issue_approval_granted.plan_my_stuff` | `issue.approve!` |
287
+ | `issue_approval_revoked.plan_my_stuff` | `issue.revoke_approval!` |
288
+ | `issue_all_approved.plan_my_stuff` | aggregate; fires when the set flips to fully approved |
289
+ | `issue_approvals_invalidated.plan_my_stuff` | aggregate; fires on revoke or new approver dropping the set |
290
+ | `comment_created.plan_my_stuff` | `Comment.create!` |
291
+ | `comment_updated.plan_my_stuff` | `comment.save!` / `comment.update!` |
292
+ | `label_added.plan_my_stuff` | `Label.add!` |
293
+ | `label_removed.plan_my_stuff` | `Label.remove!` |
294
+ | `project_item_added.plan_my_stuff` | `ProjectItem.create!` (issue or `draft: true`) |
295
+ | `project_item_removed.plan_my_stuff` | `project_item.destroy!` |
296
+ | `project_item_assigned.plan_my_stuff` | `project_item.assign!` |
297
+ | `project_item_status_changed.plan_my_stuff` | `project_item.move_to!` |
295
298
 
296
299
  ### Payload
297
300
 
@@ -304,14 +307,14 @@ All events include:
304
307
 
305
308
  Additional keys by event:
306
309
 
307
- - `issue.updated` / `comment.updated` -> `:changes` hash of `{ attr => [old, new] }`
308
- - `issue.viewers_added` / `viewers_removed` -> `:user_ids`
309
- - `issue.approval_requested` -> `:approvals` (array of newly-added `PMS::Approval`)
310
- - `issue.approval_granted` / `approval_revoked` -> `:approval` (the flipped `PMS::Approval`)
311
- - `issue.approvals_invalidated` -> `:trigger` `:revoked` or `:approver_added`
312
- - `label.added` / `label.removed` -> `:labels`
313
- - `project_item.assigned` -> `:assignees`
314
- - `project_item.status_changed` -> `:status`, `:previous_status`
310
+ - `issue_updated` / `comment_updated` -> `:changes` -- hash of `{ attr => [old, new] }`
311
+ - `issue_viewers_added` / `issue_viewers_removed` -> `:user_ids`
312
+ - `issue_approval_requested` -> `:approvals` (array of newly-added `PMS::Approval`)
313
+ - `issue_approval_granted` / `issue_approval_revoked` -> `:approval` (the flipped `PMS::Approval`)
314
+ - `issue_approvals_invalidated` -> `:trigger` -- `:revoked` or `:approver_added`
315
+ - `label_added` / `label_removed` -> `:labels`
316
+ - `project_item_assigned` -> `:assignees`
317
+ - `project_item_status_changed` -> `:status`, `:previous_status`
315
318
 
316
319
  ### Actor resolution
317
320
 
@@ -327,7 +330,7 @@ Use `config.current_user = -> { Current.user }` so request-scoped code doesn't h
327
330
 
328
331
  ```ruby
329
332
  # config/initializers/plan_my_stuff_subscribers.rb
330
- ActiveSupport::Notifications.subscribe(/^plan_my_stuff\./) do |name, _start, _finish, _id, payload|
333
+ ActiveSupport::Notifications.subscribe(/\.plan_my_stuff\z/) do |name, _start, _finish, _id, payload|
331
334
  PmsEventMailer.dispatch(name, payload).deliver_later
332
335
  end
333
336
  ```
@@ -340,13 +343,13 @@ require 'plan_my_stuff/test_helpers'
340
343
  # Block-style matcher
341
344
  expect {
342
345
  PMS::Issue.create!(title: 'Bug', body: 'Desc', user: alice)
343
- }.to(have_fired_event('plan_my_stuff.issue.created').with(user: alice))
346
+ }.to(have_fired_event('issue_created.plan_my_stuff').with(user: alice))
344
347
 
345
348
  # Raw capture
346
349
  events = PMS::TestHelpers::Notifications.capture do
347
350
  PMS::Issue.create!(...)
348
351
  end
349
- events.first[:name] # => "plan_my_stuff.issue.created"
352
+ events.first[:name] # => "issue_created.plan_my_stuff"
350
353
  events.first[:payload] # => { issue:, user:, timestamp:, ... }
351
354
  ```
352
355
 
@@ -412,7 +415,9 @@ issue.pending_approvals # [PMS::Approval(user_id: bob.id, status: 'pending')]
412
415
 
413
416
  ## Reminders
414
417
 
415
- Follow-up reminders for issues waiting on an end-user reply or pending approvers. A daily sweep fires `plan_my_stuff.issue.reminder_due` events on waiting issues whose next milestone has passed; issues that exceed the inactivity ceiling are auto-closed.
418
+ Follow-up reminders for issues waiting on an end-user reply or pending approvers. A daily sweep fires
419
+ `issue_reminder_due.plan_my_stuff` events on waiting issues whose next milestone has passed; issues that exceed the
420
+ inactivity ceiling are auto-closed.
416
421
 
417
422
  ### Waiting on user
418
423
 
@@ -433,7 +438,8 @@ A support user enters an issue into "waiting on user" state by either:
433
438
 
434
439
  Both paths add the `waiting-on-user` label, set `issue.metadata.waiting_on_user_at = now`, and schedule the first reminder.
435
440
 
436
- When an end-user posts a comment through the gem, the label is removed and the waiting state clears. If the issue had been auto-closed for inactivity, the reply also reopens it and fires `plan_my_stuff.issue.reopened_by_reply`.
441
+ When an end-user posts a comment through the gem, the label is removed and the waiting state clears. If the issue had
442
+ been auto-closed for inactivity, the reply also reopens it and fires `issue_reopened_by_reply.plan_my_stuff`.
437
443
 
438
444
  ### Waiting on approval
439
445
 
@@ -454,7 +460,7 @@ issue.metadata.reminder_days = [2, 5, 9]
454
460
  issue.save!
455
461
  ```
456
462
 
457
- Each reminder emits `plan_my_stuff.issue.reminder_due` with payload:
463
+ Each reminder emits `issue_reminder_due.plan_my_stuff` with payload:
458
464
 
459
465
  | Field | Type | Notes |
460
466
  |---|---|---|
@@ -468,7 +474,7 @@ Each reminder emits `plan_my_stuff.issue.reminder_due` with payload:
468
474
  Subscribe in your app to deliver (email, Slack, etc.):
469
475
 
470
476
  ```ruby
471
- ActiveSupport::Notifications.subscribe('plan_my_stuff.issue.reminder_due') do |event|
477
+ ActiveSupport::Notifications.subscribe('issue_reminder_due.plan_my_stuff') do |event|
472
478
  issue = event.payload[:issue]
473
479
  case event.payload[:waiting_kind]
474
480
  when :user then NotifyUserMailer.reminder(issue).deliver_later
@@ -484,9 +490,11 @@ When an issue's `days_waiting` reaches `config.inactivity_close_days` (default 3
484
490
  - Closes the issue
485
491
  - Sets `issue.metadata.closed_by_inactivity = true`
486
492
  - Adds the `user-inactive` label (configurable via `config.user_inactive_label`)
487
- - Emits `plan_my_stuff.issue.closed_inactive` (with `reason: :inactivity`) the regular `issue.closed` event is suppressed so subscribers listening to "close" don't double-fire for automated closes
493
+ - Emits `issue_closed_inactive.plan_my_stuff` (with `reason: :inactivity`); the regular `issue_closed` event is
494
+ suppressed so subscribers listening to "close" don't double-fire for automated closes
488
495
 
489
- An end-user reply on a `closed_by_inactivity` issue auto-reopens it, strips the `user-inactive` label, and fires `plan_my_stuff.issue.reopened_by_reply`.
496
+ An end-user reply on a `closed_by_inactivity` issue auto-reopens it, strips the `user-inactive` label, and fires
497
+ `issue_reopened_by_reply.plan_my_stuff`.
490
498
 
491
499
  ### Scheduling the sweep
492
500
 
@@ -534,7 +542,7 @@ Closed PMS issues that have aged past `config.archive_closed_after_days` (defaul
534
542
  2. Lock the conversation on GitHub (no new comments).
535
543
  3. Remove the issue from every Projects V2 board it sits on.
536
544
  4. Stamp `metadata.archived_at`.
537
- 5. Emit `plan_my_stuff.issue.archived` with `reason: :aged_closed`.
545
+ 5. Emit `issue_archived.plan_my_stuff` with `reason: :aged_closed`.
538
546
 
539
547
  The sweep runs inside `PMS::RemindersSweepJob` alongside the follow-up reminders sweep — same cadence, same rake task.
540
548
 
@@ -566,7 +574,7 @@ Locked issues reject new comments through the gem: `PMS::Comment.create!` raises
566
574
  ### Subscribing
567
575
 
568
576
  ```ruby
569
- ActiveSupport::Notifications.subscribe('plan_my_stuff.issue.archived') do |event|
577
+ ActiveSupport::Notifications.subscribe('issue_archived.plan_my_stuff') do |event|
570
578
  issue = event.payload[:issue]
571
579
  ArchiveMailer.notify(issue).deliver_later
572
580
  end
@@ -161,7 +161,7 @@ module PlanMyStuff
161
161
  end
162
162
 
163
163
  # Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev dragging an item from "Submitted" to
164
- # "Started" on the project board into a +Pipeline.take!+ call so the +plan_my_stuff.pipeline.started+
164
+ # "Started" on the project board into a +Pipeline.take!+ call so the +pipeline_started.plan_my_stuff+
165
165
  # event fires.
166
166
  #
167
167
  # Only acts on +action: 'edited'+ where the Status single-select field changed on the pipeline project.
@@ -108,7 +108,7 @@ PlanMyStuff.configure do |config|
108
108
  # --------------------------------------------------------------------------
109
109
  # Notification actor
110
110
  # --------------------------------------------------------------------------
111
- # Fallback actor for notification events (plan_my_stuff.*) when a caller
111
+ # Fallback actor for notification events (*.plan_my_stuff) when a caller
112
112
  # does not pass an explicit user: kwarg. Proc/lambda called at event time.
113
113
  # config.current_user = -> { Current.user }
114
114
 
@@ -215,7 +215,7 @@ PlanMyStuff.configure do |config|
215
215
  # --------------------------------------------------------------------------
216
216
  # Follow-up reminders
217
217
  # --------------------------------------------------------------------------
218
- # Daily sweep that fires `plan_my_stuff.issue.reminder_due` events on
218
+ # Daily sweep that fires `issue_reminder_due.plan_my_stuff` events on
219
219
  # waiting issues and auto-closes issues that exceed the inactivity
220
220
  # ceiling. Consuming app is responsible for the initial enqueue:
221
221
  #
@@ -251,7 +251,7 @@ PlanMyStuff.configure do |config|
251
251
  # `archive_closed_after_days` ago (based on GitHub's `closed_at`) the
252
252
  # sweep: adds the configured label, locks the conversation, removes
253
253
  # the issue from every Projects V2 board, stamps `metadata.archived_at`,
254
- # and emits `plan_my_stuff.issue.archived` with `reason: :aged_closed`.
254
+ # and emits `issue_archived.plan_my_stuff` with `reason: :aged_closed`.
255
255
  # Issues auto-closed by the inactivity sweep (closed_by_inactivity) are
256
256
  # excluded. Non-PMS issues are excluded.
257
257
  #
@@ -109,7 +109,7 @@ module PlanMyStuff
109
109
  add_item!(issue: issue_or_title, project_number: project_number)
110
110
  end
111
111
 
112
- PlanMyStuff::Notifications.instrument('project_item.added', item, user: user)
112
+ PlanMyStuff::Notifications.instrument('project_item_added', item, user: user)
113
113
  item
114
114
  end
115
115
 
@@ -463,7 +463,7 @@ module PlanMyStuff
463
463
  )
464
464
 
465
465
  PlanMyStuff::Notifications.instrument(
466
- 'project_item.status_changed',
466
+ 'project_item_status_changed',
467
467
  self,
468
468
  user: user,
469
469
  status: status,
@@ -520,7 +520,7 @@ module PlanMyStuff
520
520
  item_id: id,
521
521
  )
522
522
 
523
- PlanMyStuff::Notifications.instrument('project_item.removed', self, user: user)
523
+ PlanMyStuff::Notifications.instrument('project_item_removed', self, user: user)
524
524
  destroyed!
525
525
  deleted_id
526
526
  end
@@ -544,7 +544,7 @@ module PlanMyStuff
544
544
  )
545
545
 
546
546
  PlanMyStuff::Notifications.instrument(
547
- 'project_item.assigned',
547
+ 'project_item_assigned',
548
548
  self,
549
549
  user: user,
550
550
  assignees: assignee_list,
@@ -94,7 +94,7 @@ module PlanMyStuff
94
94
  mark_issue_responded_if_first_support_comment!(issue, resolved_user) unless skip_responded
95
95
 
96
96
  comment = build(result, issue: issue)
97
- PlanMyStuff::Notifications.instrument('comment.created', comment, user: resolved_user)
97
+ PlanMyStuff::Notifications.instrument('comment_created', comment, user: resolved_user)
98
98
  apply_waiting_state_transitions!(issue, resolved_user, waiting_on_reply, comment)
99
99
  comment
100
100
  end
@@ -324,7 +324,7 @@ module PlanMyStuff
324
324
  self.class.update!(id: id, repo: issue.repo, body: serialized)
325
325
 
326
326
  reload
327
- PlanMyStuff::Notifications.instrument('comment.updated', self, user: user, changes: captured_changes)
327
+ PlanMyStuff::Notifications.instrument('comment_updated', self, user: user, changes: captured_changes)
328
328
  self
329
329
  end
330
330
 
@@ -166,7 +166,7 @@ module PlanMyStuff
166
166
  issue.set_issue_fields!(issue_fields) if issue_fields.present?
167
167
 
168
168
  issue.reload
169
- PlanMyStuff::Notifications.instrument('issue.created', issue, user: user)
169
+ PlanMyStuff::Notifications.instrument('issue_created', issue, user: user)
170
170
  issue
171
171
  end
172
172
 
@@ -509,7 +509,7 @@ module PlanMyStuff
509
509
  end
510
510
 
511
511
  # Tags the issue with the configured +archived_label+, removes it from every Projects V2 board it belongs to,
512
- # locks its conversation on GitHub, and stamps +metadata.archived_at+. Emits +plan_my_stuff.issue.archived+ on
512
+ # locks its conversation on GitHub, and stamps +metadata.archived_at+. Emits +issue_archived.plan_my_stuff+ on
513
513
  # success.
514
514
  #
515
515
  # No-op (no network calls, no event) when the issue is already archived (either +metadata.archived_at+ is set or
@@ -542,7 +542,7 @@ module PlanMyStuff
542
542
  reload
543
543
 
544
544
  PlanMyStuff::Notifications.instrument(
545
- 'issue.archived',
545
+ 'issue_archived',
546
546
  self,
547
547
  reason: :aged_closed,
548
548
  )
@@ -786,11 +786,11 @@ module PlanMyStuff
786
786
  def instrument_update(captured, user)
787
787
  case captured['state']
788
788
  when %w[open closed]
789
- PlanMyStuff::Notifications.instrument('issue.closed', self, user: user)
789
+ PlanMyStuff::Notifications.instrument('issue_closed', self, user: user)
790
790
  when %w[closed open]
791
- PlanMyStuff::Notifications.instrument('issue.reopened', self, user: user)
791
+ PlanMyStuff::Notifications.instrument('issue_reopened', self, user: user)
792
792
  else
793
- PlanMyStuff::Notifications.instrument('issue.updated', self, user: user, changes: captured)
793
+ PlanMyStuff::Notifications.instrument('issue_updated', self, user: user, changes: captured)
794
794
  end
795
795
  end
796
796
 
@@ -33,8 +33,8 @@ module PlanMyStuff
33
33
  # Adds approvers to this issue's required-approvals list. Idempotent: users already present are no-ops. Only
34
34
  # support users may call this.
35
35
  #
36
- # Fires +plan_my_stuff.issue.approval_requested+ when any user is newly added. Also fires
37
- # +plan_my_stuff.issue.approvals_invalidated+ (+trigger: :approver_added+) when the new approvers flip the issue
36
+ # Fires +issue_approval_requested.plan_my_stuff+ when any user is newly added. Also fires
37
+ # +issue_approvals_invalidated.plan_my_stuff+ (+trigger: :approver_added+) when the new approvers flip the issue
38
38
  # out of a fully-approved state.
39
39
  #
40
40
  # @param user_ids [Array<Integer>, Integer]
@@ -81,8 +81,8 @@ module PlanMyStuff
81
81
  end
82
82
 
83
83
  # Flips the caller's approval to +approved+ from any other state (+pending+ or +rejected+). Only the approver
84
- # themselves may call this. Fires +plan_my_stuff.issue.approval_granted+ and, when this flip completes the
85
- # approval set, +plan_my_stuff.issue.all_approved+.
84
+ # themselves may call this. Fires +issue_approval_granted.plan_my_stuff+ and, when this flip completes the
85
+ # approval set, +issue_all_approved.plan_my_stuff+.
86
86
  #
87
87
  # @raise [PlanMyStuff::ValidationError] when the caller is not in the approvers list or is already approved
88
88
  #
@@ -109,9 +109,9 @@ module PlanMyStuff
109
109
  end
110
110
 
111
111
  # Flips the caller's approval to +rejected+ from any other state (+pending+ or +approved+). Only the approver
112
- # themselves may call this. Fires +plan_my_stuff.issue.approval_rejected+ and, when this flip drops the issue
112
+ # themselves may call this. Fires +issue_approval_rejected.plan_my_stuff+ and, when this flip drops the issue
113
113
  # out of +fully_approved?+ (i.e. the caller was the last +approved+ approver),
114
- # +plan_my_stuff.issue.approvals_invalidated+ (+trigger: :rejected+).
114
+ # +issue_approvals_invalidated.plan_my_stuff+ (+trigger: :rejected+).
115
115
  #
116
116
  # @raise [PlanMyStuff::ValidationError] when the caller is not in the approvers list or is already rejected
117
117
  #
@@ -147,9 +147,9 @@ module PlanMyStuff
147
147
  # may revoke any approver's response by passing +target_user_id:+. Non-support callers passing a
148
148
  # +target_user_id:+ that is not their own raise +AuthorizationError+.
149
149
  #
150
- # Emits the granular event keyed off the source state: +plan_my_stuff.issue.approval_revoked+ from approved, or
151
- # +plan_my_stuff.issue.rejection_revoked+ from rejected. When revoking an approval drops the issue out of
152
- # +fully_approved?+, also fires +plan_my_stuff.issue.approvals_invalidated+ (+trigger: :revoked+). Revoking a
150
+ # Emits the granular event keyed off the source state: +issue_approval_revoked.plan_my_stuff+ from approved, or
151
+ # +issue_rejection_revoked.plan_my_stuff+ from rejected. When revoking an approval drops the issue out of
152
+ # +fully_approved?+, also fires +issue_approvals_invalidated.plan_my_stuff+ (+trigger: :revoked+). Revoking a
153
153
  # rejection cannot change +fully_approved?+ (the issue was already gated), so no aggregate event fires.
154
154
  #
155
155
  # @raise [PlanMyStuff::AuthorizationError] when a non-support caller targets another user
@@ -312,7 +312,7 @@ module PlanMyStuff
312
312
  return if added.empty?
313
313
 
314
314
  PlanMyStuff::Notifications.instrument(
315
- 'issue.approval_requested',
315
+ 'issue_approval_requested',
316
316
  self,
317
317
  user: user,
318
318
  approvals: added,
@@ -333,7 +333,7 @@ module PlanMyStuff
333
333
  #
334
334
  def finish_state_change(event, approval, user:, was_fully_approved:, trigger: nil)
335
335
  PlanMyStuff::Notifications.instrument(
336
- "issue.#{event}",
336
+ "issue_#{event}",
337
337
  self,
338
338
  user: user,
339
339
  approval: approval,
@@ -355,10 +355,10 @@ module PlanMyStuff
355
355
  now = fully_approved?
356
356
 
357
357
  if !was_fully_approved && now
358
- PlanMyStuff::Notifications.instrument('issue.all_approved', self, user: user)
358
+ PlanMyStuff::Notifications.instrument('issue_all_approved', self, user: user)
359
359
  elsif was_fully_approved && !now && approvals_required?
360
360
  PlanMyStuff::Notifications.instrument(
361
- 'issue.approvals_invalidated',
361
+ 'issue_approvals_invalidated',
362
362
  self,
363
363
  user: user,
364
364
  trigger: trigger,
@@ -210,7 +210,7 @@ module PlanMyStuff
210
210
  # 6. Closes self with +state_reason: :duplicate+ and
211
211
  # +duplicate_of: { owner:, repo:, number: }+.
212
212
  # 7. Reloads self; invalidates link caches.
213
- # 8. Fires +plan_my_stuff.issue.marked_duplicate+.
213
+ # 8. Fires +issue_marked_duplicate.plan_my_stuff+.
214
214
  #
215
215
  # Partial failures are not rolled back - GitHub retains whatever side effects succeeded before the failing step.
216
216
  #
@@ -232,7 +232,7 @@ module PlanMyStuff
232
232
 
233
233
  reload
234
234
  invalidate_links_cache!
235
- PlanMyStuff::Notifications.instrument('issue.marked_duplicate', self, target: target_issue, user: user)
235
+ PlanMyStuff::Notifications.instrument('issue_marked_duplicate', self, target: target_issue, user: user)
236
236
 
237
237
  build_link!(target_issue, type: :duplicate_of)
238
238
  end
@@ -303,7 +303,7 @@ module PlanMyStuff
303
303
  end
304
304
 
305
305
  # Attempts the reciprocal write on +link+'s target. On failure, fires
306
- # +plan_my_stuff.issue.link_reciprocal_failed+ so the consuming app can surface the half-written pairing.
306
+ # +issue_link_reciprocal_failed.plan_my_stuff+ so the consuming app can surface the half-written pairing.
307
307
  #
308
308
  # @param link [PlanMyStuff::Link]
309
309
  # @param user [Object, nil]
@@ -315,7 +315,7 @@ module PlanMyStuff
315
315
  yield(target)
316
316
  rescue PlanMyStuff::Error, Octokit::Error => e
317
317
  PlanMyStuff::Notifications.instrument(
318
- 'issue.link_reciprocal_failed',
318
+ 'issue_link_reciprocal_failed',
319
319
  self,
320
320
  user: user,
321
321
  link: link,
@@ -6,7 +6,7 @@ module PlanMyStuff
6
6
  # Adds user IDs to this issue's visibility allowlist (non-support users whose ID is in the allowlist can see
7
7
  # internal comments).
8
8
  #
9
- # Fires +plan_my_stuff.issue.viewers_added+.
9
+ # Fires +issue_viewers_added.plan_my_stuff+.
10
10
  #
11
11
  # @param user_ids [Array<Integer>, Integer]
12
12
  # @param user [Object, nil] actor for the notification event
@@ -16,13 +16,13 @@ module PlanMyStuff
16
16
  def add_viewers!(user_ids:, user: nil)
17
17
  ids = Array.wrap(user_ids)
18
18
  modify_allowlist! { |allowlist| allowlist | ids }
19
- PlanMyStuff::Notifications.instrument('issue.viewers_added', self, user: user, user_ids: ids)
19
+ PlanMyStuff::Notifications.instrument('issue_viewers_added', self, user: user, user_ids: ids)
20
20
  metadata.visibility_allowlist
21
21
  end
22
22
 
23
23
  # Removes user IDs from this issue's visibility allowlist.
24
24
  #
25
- # Fires +plan_my_stuff.issue.viewers_removed+.
25
+ # Fires +issue_viewers_removed.plan_my_stuff+.
26
26
  #
27
27
  # @param user_ids [Array<Integer>, Integer]
28
28
  # @param user [Object, nil] actor for the notification event
@@ -32,7 +32,7 @@ module PlanMyStuff
32
32
  def remove_viewers!(user_ids:, user: nil)
33
33
  ids = Array.wrap(user_ids)
34
34
  modify_allowlist! { |allowlist| allowlist - ids }
35
- PlanMyStuff::Notifications.instrument('issue.viewers_removed', self, user: user, user_ids: ids)
35
+ PlanMyStuff::Notifications.instrument('issue_viewers_removed', self, user: user, user_ids: ids)
36
36
  metadata.visibility_allowlist
37
37
  end
38
38
 
@@ -55,7 +55,7 @@ module PlanMyStuff
55
55
  end
56
56
 
57
57
  # Reopens an issue that was auto-closed by the inactivity sweep, clears +metadata.closed_by_inactivity+, and
58
- # emits +plan_my_stuff.issue.reopened_by_reply+ carrying the reopening comment. Does not emit the regular
58
+ # emits +issue_reopened_by_reply.plan_my_stuff+ carrying the reopening comment. Does not emit the regular
59
59
  # +issue.reopened+ event \- subscribers that specifically care about this flow subscribe to the dedicated event.
60
60
  #
61
61
  # @param comment [PlanMyStuff::Comment] the reopening comment
@@ -76,7 +76,7 @@ module PlanMyStuff
76
76
  reload
77
77
 
78
78
  PlanMyStuff::Notifications.instrument(
79
- 'issue.reopened_by_reply',
79
+ 'issue_reopened_by_reply',
80
80
  self,
81
81
  user: user,
82
82
  comment: comment,
@@ -28,7 +28,7 @@ module PlanMyStuff
28
28
  PlanMyStuff::Cache.delete_issue(issue.repo, issue.number)
29
29
 
30
30
  PlanMyStuff::Notifications.instrument(
31
- 'label.added', issue, user: user, labels: label_names,
31
+ 'label_added', issue, user: user, labels: label_names,
32
32
  )
33
33
 
34
34
  result.map { |gh_label| build(gh_label, issue: issue) }
@@ -90,7 +90,7 @@ module PlanMyStuff
90
90
  PlanMyStuff::Cache.delete_issue(issue.repo, issue.number)
91
91
 
92
92
  PlanMyStuff::Notifications.instrument(
93
- 'label.removed', issue, user: user, labels: label_names,
93
+ 'label_removed', issue, user: user, labels: label_names,
94
94
  )
95
95
 
96
96
  results
@@ -7,18 +7,20 @@ module PlanMyStuff
7
7
  # +PlanMyStuff::Notifications.instrument+ at mutation points so
8
8
  # consuming apps can subscribe for email, webhooks, Slack, etc.
9
9
  #
10
- # Events are fired under the +plan_my_stuff.<event>+ namespace via
11
- # +ActiveSupport::Notifications+. Subscribers run synchronously.
10
+ # Events are fired under the +<event>.plan_my_stuff+ namespace via
11
+ # +ActiveSupport::Notifications+ (Rails convention: event first,
12
+ # library last - matches +sql.active_record+, +deliver.action_mailer+).
13
+ # Subscribers run synchronously.
12
14
  #
13
15
  module Notifications
14
16
  module_function
15
17
 
16
- EVENT_PREFIX = 'plan_my_stuff'
18
+ EVENT_SUFFIX = 'plan_my_stuff'
17
19
  SKIPPED_LOG_KEYS = %i[user timestamp visibility visibility_allowlist].freeze
18
20
 
19
- # Fires +plan_my_stuff.<event>+ with a normalized payload.
21
+ # Fires +<event>.plan_my_stuff+ with a normalized payload.
20
22
  #
21
- # @param event [String] e.g. +'issue.created'+
23
+ # @param event [String] e.g. +'issue_created'+
22
24
  # @param resource [Object] domain object (+Issue+, +Comment+, +ProjectItem+, ...)
23
25
  # @param user [Object, nil] explicit actor; falls back to +config.current_user+
24
26
  # @param extra [Hash] additional payload entries (+changes:+, +labels:+, +user_ids:+, ...)
@@ -29,7 +31,7 @@ module PlanMyStuff
29
31
  actor = user || resolve_current_user
30
32
  payload = build_payload(resource, actor, extra)
31
33
  log(event, payload)
32
- ActiveSupport::Notifications.instrument("#{EVENT_PREFIX}.#{event}", payload)
34
+ ActiveSupport::Notifications.instrument("#{event}.#{EVENT_SUFFIX}", payload)
33
35
  end
34
36
 
35
37
  # Invokes +config.current_user+ if it responds to +call+.
@@ -113,7 +115,7 @@ module PlanMyStuff
113
115
  logger = rails_logger
114
116
  return if logger.nil?
115
117
 
116
- logger.debug { "[PlanMyStuff] #{EVENT_PREFIX}.#{event} #{log_fields(payload)}" }
118
+ logger.debug { "[PlanMyStuff] #{event}.#{EVENT_SUFFIX} #{log_fields(payload)}" }
117
119
  end
118
120
 
119
121
  # @return [Logger, nil]
@@ -7,7 +7,7 @@ module PlanMyStuff
7
7
  #
8
8
  # Driven by +RemindersSweepJob+ so the gem ships one scheduled
9
9
  # entrypoint. Items are removed via +Pipeline.remove!+ so subscribers
10
- # see the standard +plan_my_stuff.pipeline.removed+ event.
10
+ # see the standard +pipeline_removed.plan_my_stuff+ event.
11
11
  #
12
12
  module CompletedSweep
13
13
  module_function
@@ -49,7 +49,7 @@ module PlanMyStuff
49
49
  PlanMyStuff.configuration.pipeline_statuses.fetch(canonical, canonical)
50
50
  end
51
51
 
52
- # Fires a +plan_my_stuff.pipeline.<event>+ notification.
52
+ # Fires a +pipeline_<event>.plan_my_stuff+ notification.
53
53
  #
54
54
  # When +event+ is a canonical +Pipeline::Status+ name, the event key is derived via +Status.key_for+ and the
55
55
  # canonical status is added to the payload as +:status+. Otherwise +event+ is used verbatim as the suffix (e.g.
@@ -70,7 +70,7 @@ module PlanMyStuff
70
70
  end
71
71
 
72
72
  PlanMyStuff::Notifications.instrument(
73
- "pipeline.#{event_to_use}",
73
+ "pipeline_#{event_to_use}",
74
74
  project_item,
75
75
  issue_number: project_item.number,
76
76
  **extra_to_use,
@@ -98,7 +98,7 @@ module PlanMyStuff
98
98
  # Removes a project item from the pipeline project entirely. Captures the prior status before deletion so
99
99
  # subscribers can decide whether the removal happened late in the lifecycle.
100
100
  #
101
- # Always fires +plan_my_stuff.pipeline.removed+. Additionally fires +plan_my_stuff.pipeline.removed_late+ when
101
+ # Always fires +pipeline_removed.plan_my_stuff+. Additionally fires +pipeline_removed_late.plan_my_stuff+ when
102
102
  # +prior_status+ is past "Started" (i.e. anything other than "Started", accounting for configured status aliases).
103
103
  # +nil+ status is treated as not-late.
104
104
  #
@@ -4,7 +4,7 @@ module PlanMyStuff
4
4
  module Reminders
5
5
  # Auto-closes a waiting issue that has exceeded the configured
6
6
  # +inactivity_close_days+ ceiling. Clears waiting state and emits a
7
- # dedicated +plan_my_stuff.issue.closed_inactive+ event.
7
+ # dedicated +issue_closed_inactive.plan_my_stuff+ event.
8
8
  #
9
9
  # Goes through +Issue#update!+ with +skip_notification: true+ so the
10
10
  # regular +issue.closed+ event is suppressed in favor of the
@@ -37,7 +37,7 @@ module PlanMyStuff
37
37
 
38
38
  # Closes the issue, tags it with the configured
39
39
  # +user_inactive_label+, then emits
40
- # +plan_my_stuff.issue.closed_inactive+.
40
+ # +issue_closed_inactive.plan_my_stuff+.
41
41
  #
42
42
  # @return [void]
43
43
  #
@@ -49,7 +49,7 @@ module PlanMyStuff
49
49
  )
50
50
  add_user_inactive_label
51
51
  PlanMyStuff::Notifications.instrument(
52
- 'issue.closed_inactive',
52
+ 'issue_closed_inactive',
53
53
  @issue,
54
54
  reason: :inactivity,
55
55
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  module Reminders
5
- # Emits +plan_my_stuff.issue.reminder_due+ for a single waiting issue
5
+ # Emits +issue_reminder_due.plan_my_stuff+ for a single waiting issue
6
6
  # and advances its +next_reminder_at+ to the next milestone in the
7
7
  # effective +reminder_days+ schedule (or +nil+ when the last milestone
8
8
  # has passed).
@@ -34,7 +34,7 @@ module PlanMyStuff
34
34
  #
35
35
  def call
36
36
  payload = build_payload
37
- PlanMyStuff::Notifications.instrument('issue.reminder_due', @issue, **payload)
37
+ PlanMyStuff::Notifications.instrument('issue_reminder_due', @issue, **payload)
38
38
 
39
39
  @issue.update!(
40
40
  metadata: { next_reminder_at: next_reminder_at_value },
@@ -177,7 +177,7 @@ module PlanMyStuff
177
177
  end
178
178
  end
179
179
 
180
- # Captures +plan_my_stuff.*+ +ActiveSupport::Notifications+ events fired
180
+ # Captures +*.plan_my_stuff+ +ActiveSupport::Notifications+ events fired
181
181
  # inside the given block. Lets consuming apps assert that their code
182
182
  # triggers gem events without threading subscriptions through every spec.
183
183
  #
@@ -186,7 +186,7 @@ module PlanMyStuff
186
186
  # events = PlanMyStuff::TestHelpers::Notifications.capture do
187
187
  # PlanMyStuff::Issue.create!(...)
188
188
  # end
189
- # events.first[:name] # => "plan_my_stuff.issue.created"
189
+ # events.first[:name] # => "issue_created.plan_my_stuff"
190
190
  # events.first[:payload] # => { issue:, user:, timestamp:, ... }
191
191
  #
192
192
  module Notifications
@@ -196,7 +196,7 @@ module PlanMyStuff
196
196
  def capture(&)
197
197
  events = []
198
198
  callback = -> (name, _start, _finish, _id, payload) { events << { name: name, payload: payload } }
199
- ActiveSupport::Notifications.subscribed(callback, /^plan_my_stuff\./, &)
199
+ ActiveSupport::Notifications.subscribed(callback, /\.plan_my_stuff\z/, &)
200
200
  events
201
201
  end
202
202
  end
@@ -708,11 +708,11 @@ module PlanMyStuff
708
708
  end
709
709
 
710
710
  if defined?(RSpec::Matchers)
711
- # Asserts that a +plan_my_stuff.<name>+ event was fired inside the block.
711
+ # Asserts that a +<name>.plan_my_stuff+ event was fired inside the block.
712
712
  # Chain +.with(key: value, ...)+ to match on a payload subset.
713
713
  #
714
- # expect { Issue.create!(...) }.to(have_fired_event('plan_my_stuff.issue.created'))
715
- # expect { Issue.create!(...) }.to(have_fired_event('plan_my_stuff.issue.created').with(user: alice))
714
+ # expect { Issue.create!(...) }.to(have_fired_event('issue_created.plan_my_stuff'))
715
+ # expect { Issue.create!(...) }.to(have_fired_event('issue_created.plan_my_stuff').with(user: alice))
716
716
  #
717
717
  RSpec::Matchers.define(:have_fired_event) do |event_name|
718
718
  supports_block_expectations
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 14
6
+ MINOR = 15
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_my_stuff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance