plan_my_stuff 0.1.0 → 1.0.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +595 -0
  3. data/CONFIGURATION.md +487 -0
  4. data/README.md +612 -88
  5. data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
  11. data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
  12. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
  13. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
  14. data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
  15. data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
  16. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
  17. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
  18. data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
  19. data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
  20. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
  21. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
  22. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
  23. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
  24. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
  25. data/app/jobs/plan_my_stuff/application_job.rb +8 -0
  26. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
  27. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  28. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
  29. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
  31. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  32. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
  33. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
  34. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
  35. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
  36. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
  37. data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
  38. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  39. data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
  40. data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
  41. data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
  42. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
  43. data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
  44. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
  45. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
  46. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
  47. data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
  50. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
  51. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  52. data/config/routes.rb +43 -15
  53. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
  54. data/lib/plan_my_stuff/application_record.rb +158 -1
  55. data/lib/plan_my_stuff/approval.rb +88 -0
  56. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  57. data/lib/plan_my_stuff/archive.rb +12 -0
  58. data/lib/plan_my_stuff/attachment.rb +83 -0
  59. data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
  60. data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
  61. data/lib/plan_my_stuff/base_metadata.rb +25 -28
  62. data/lib/plan_my_stuff/base_project.rb +502 -0
  63. data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
  64. data/lib/plan_my_stuff/base_project_item.rb +588 -0
  65. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  66. data/lib/plan_my_stuff/cache.rb +197 -0
  67. data/lib/plan_my_stuff/client.rb +139 -64
  68. data/lib/plan_my_stuff/comment.rb +225 -100
  69. data/lib/plan_my_stuff/comment_metadata.rb +68 -5
  70. data/lib/plan_my_stuff/configuration.rb +459 -28
  71. data/lib/plan_my_stuff/custom_fields.rb +96 -12
  72. data/lib/plan_my_stuff/engine.rb +14 -2
  73. data/lib/plan_my_stuff/errors.rb +65 -5
  74. data/lib/plan_my_stuff/graphql/queries.rb +454 -0
  75. data/lib/plan_my_stuff/issue.rb +1097 -166
  76. data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
  77. data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
  78. data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
  79. data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
  80. data/lib/plan_my_stuff/issue_field.rb +126 -0
  81. data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
  82. data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
  83. data/lib/plan_my_stuff/issue_metadata.rb +132 -21
  84. data/lib/plan_my_stuff/label.rb +100 -13
  85. data/lib/plan_my_stuff/link.rb +144 -0
  86. data/lib/plan_my_stuff/markdown.rb +13 -7
  87. data/lib/plan_my_stuff/metadata_parser.rb +51 -12
  88. data/lib/plan_my_stuff/notifications.rb +148 -0
  89. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
  90. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  91. data/lib/plan_my_stuff/pipeline/status.rb +40 -0
  92. data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
  93. data/lib/plan_my_stuff/pipeline.rb +310 -0
  94. data/lib/plan_my_stuff/project.rb +63 -465
  95. data/lib/plan_my_stuff/project_item.rb +3 -409
  96. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  97. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  98. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  99. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  100. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  101. data/lib/plan_my_stuff/reminders.rb +12 -0
  102. data/lib/plan_my_stuff/repo.rb +145 -0
  103. data/lib/plan_my_stuff/test_helpers.rb +265 -25
  104. data/lib/plan_my_stuff/testing_project.rb +292 -0
  105. data/lib/plan_my_stuff/testing_project_item.rb +218 -0
  106. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  107. data/lib/plan_my_stuff/user_resolver.rb +24 -3
  108. data/lib/plan_my_stuff/verifier.rb +10 -0
  109. data/lib/plan_my_stuff/version.rb +2 -2
  110. data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
  111. data/lib/plan_my_stuff.rb +55 -20
  112. data/lib/tasks/plan_my_stuff.rake +331 -0
  113. metadata +99 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03731d4c3b552639604aa37465eb9f2246eb6077ec40144b218b4afa2aefdc01
4
- data.tar.gz: 0277ebbdc6a0f783c90cb1bcf2d73c6dfd79447ad52f1df8d547d217cf80e1bf
3
+ metadata.gz: 3993e1306f27793791833787c546d81a42ed7bdf23d1420a658ec3877acf284b
4
+ data.tar.gz: 4593de2a00ea81ad22d0d951e1a97eb858a3d26071510c42e2e1aab9b99ae0f8
5
5
  SHA512:
6
- metadata.gz: 3897dad435fd7b13780d3ac6ac3e25f9d15e49f58e8517c05ff6b28b78b7efd16216e4c9dbf0275afadf842821a9a006dd0e1c86b862e10927e197f190029f6a
7
- data.tar.gz: 1a973153e39fc469580eac16dca37debb9780416b1cc7b50f9dffd440a6f64f4383fca30f24e72fdc02173c5875938c9181febd0226de8ecc8023c9907ac10db
6
+ metadata.gz: a9e140d9c6371186961903c5251143a63f4bedb3c238fc2d2b69e25ef3263824bdeacf767494c46473a5c7e9560c50b057e9b82136505b3615bcacdc742b3b6c
7
+ data.tar.gz: f73cb36b19ced07d713ae6e1aee02ab314292b3410d8b14d160e2156ceedd4a202f2fcecff7f67b61d25c93887bbc7e694f4415b89bab1203faa1e8bf7d7566c
data/CHANGELOG.md ADDED
@@ -0,0 +1,595 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ Official release.
6
+
7
+ ### Breaking
8
+
9
+ - `MetadataParser.parse` no longer recognizes the legacy `<!-- pms-metadata:... -->` HTML-comment format.
10
+ Bodies written before 0.17.0 that have not been re-saved will now be returned as plain body text with
11
+ empty metadata. The deprecation warning emitted by 0.17.0+ has been removed along with the legacy
12
+ pattern.
13
+ - Removed the deprecated `IssueMetadata#priority_list`, `#priority_list=`, `#priority_list?`,
14
+ `#priority_list_priority`, and `#priority_list_priority=` accessors, along with the legacy-write forwarding
15
+ that turned `metadata.priority_list = true` into `Priority List` / `Priority List Priority` issue-field writes
16
+ on `save!` / `update!` / `create!`. Use `Issue#priority_list?` / `#priority_list_priority` for reads and
17
+ `Issue#add_to_priority_list!(priority: N)` / `#remove_from_priority_list!` for writes (closes #57).
18
+ - `CommentMetadata` no longer parses the legacy `{filename, url}` attachment shape into structured fields.
19
+ Entries must now carry the `{filename, owner, repo, sha, path}` shape; legacy entries that were never
20
+ re-saved are dropped as malformed. The `LEGACY_URL_REGEXES` / `LEGACY_ATTACHMENT_DEPRECATION_MESSAGE`
21
+ constants and the migrate-on-write deprecation warning have been removed.
22
+
23
+ ## 0.30.0
24
+
25
+ ### Added
26
+
27
+ - The AWS deployment-completed webhook now fires a single `pipeline_deployment_completed.plan_my_stuff` batch event
28
+ after the per-item sweep, carrying every item that actually transitioned (payload: `:project_items`, `:issue_numbers`,
29
+ `:commit_sha`). Per-item `pipeline_completed.plan_my_stuff` events still fire. The batch event is skipped when nothing
30
+ transitioned, giving subscribers (release-notes mailer, Slack summary, changelog generator) one clean
31
+ "deployment finished" signal without debouncing N per-item events (closes #97).
32
+
33
+ ### Changed
34
+
35
+ - `PlanMyStuff::Notifications.instrument` now accepts an `Array` resource. The payload key is the pluralized element
36
+ key (a batch of project items keys as `:project_items`, a batch of issues as `:issues`), so batch events carry a full
37
+ set under one key.
38
+ - `PlanMyStuff::Pipeline.instrument` now accepts an `Array` of items for a batch event; the payload carries
39
+ `:issue_numbers` (the linked number of every item) in place of the single-item `:issue_number`.
40
+
41
+ ## 0.29.0
42
+
43
+ ### Changed
44
+
45
+ - `Pipeline.complete_deployment!` now sets the linked issue's `Issue Status` field to `Fixed` on the auto-complete
46
+ success path (alongside moving the item to `Completed`). Gated on `config.issue_fields_enabled`, so completion still
47
+ succeeds when issue fields are disabled. The auto-complete-off early return is unchanged (closes #96).
48
+
49
+ ## 0.28.0
50
+
51
+ ### Added
52
+
53
+ - `Issue.list_page_info(...)` returns a `PlanMyStuff::Issue::PageInfo` value object exposing `.issues`, `.page`,
54
+ `.per_page`, `.has_next?`, and `.has_prev?`. `Issue.list` now delegates to it; return type unchanged
55
+ (`Array<Issue>`). Closes #63.
56
+
57
+ ## 0.27.0
58
+
59
+ ### Added
60
+
61
+ - `PlanMyStuff::Issue#mark_responded!(user)` - stamps `metadata.responded_at` on the first support-user engagement of
62
+ any kind. No-ops when the user is blank, not a support user, the issue isn't a PMS issue, or `responded_at` is
63
+ already set.
64
+
65
+ ### Changed
66
+
67
+ - `Pipeline.take!` and the `issues.assigned` webhook now stamp `metadata.responded_at` on first support engagement
68
+ (was previously only set by the first support comment) (closes #60).
69
+
70
+ ## 0.26.0
71
+
72
+ ### Added
73
+
74
+ - `PlanMyStuff::Issue#awaiting_reply?` - returns `true` when the issue's `Issue Status` field is `Waiting on Reply`.
75
+ - `config.issue_field_names` (canonical field name => consumer field name) and `config.issue_field_values` (canonical
76
+ field name => `{ canonical value => consumer value }`) let a consuming org rename the native Issue Fields / option
77
+ labels the gem refers to internally. Translation runs via the new `PlanMyStuff::IssueFieldTranslation` module:
78
+ canonical => consumer on writes and filters (`set_issue_fields!`, `Issue.list` / `.count` `issue_fields:` +
79
+ `priority_list:`), consumer => canonical on reads (`Issue#issue_fields`), so internal comparisons like
80
+ `awaiting_reply?` / `priority_list?` keep working regardless of the org's naming. Unmapped names / values pass
81
+ through unchanged (identity fallback, mirroring `config.pipeline_statuses`).
82
+
83
+ ### Changed
84
+
85
+ - The waiting/inactivity lifecycle methods now write the native `Issue Status` issue field alongside their existing
86
+ label/metadata changes (gated on `config.issue_fields_enabled`): `enter_waiting_on_user!` sets `Waiting on Reply`,
87
+ `clear_waiting_on_user!` sets `Open`, and `reopen_by_reply!` sets `Reopened`. No-op when `issue_fields_enabled` is
88
+ `false`.
89
+
90
+ ## 0.25.0
91
+
92
+ ### Added
93
+
94
+ - `PlanMyStuff::Pipeline.clear_testing!(project_item, user: nil)` - reverses `request_testing!` by writing the
95
+ `:inactive` Testing field value. Fires `pipeline_testing_cleared.plan_my_stuff` with `field_name`, `value`,
96
+ `user` payload.
97
+ - `PlanMyStuff::Issues::TestingsController` + `POST/DELETE /issues/:issue_id/testing` route (gated on
98
+ `config.pipeline_enabled`). Backs new "Request testing" / "Clear testing" buttons on the issue show view
99
+ (closes #52).
100
+
101
+ ## 0.24.0
102
+
103
+ ### Added
104
+
105
+ - `PlanMyStuff::Issue.list` and `PlanMyStuff::Issue.count` now accept `issue_type:` (String / Symbol) and
106
+ `issue_fields:` (Hash keyed by display name) filter kwargs. `issue_type:` resolves symbol nicknames through
107
+ `ISSUE_TYPE_NICKNAMES` and `config.issue_types`; Arrays raise `ArgumentError` since GitHub's REST `type` param
108
+ and Search `type:` qualifier each accept only one value. `issue_fields:` accepts scalar (equality) or `Range`
109
+ (date / numeric bounds -- inclusive
110
+ `..` -> `>=`/`<=`, exclusive `...` -> `>=`/`<`, beginless / endless ranges drop the unbounded side) values and ANDs
111
+ multiple constraints together. Composes with the existing `priority_list:` filter into the same
112
+ `issue_field_values` (REST) / `field.<slug>:` (Search) qualifier list. Search-API calls flip
113
+ `advanced_search=true` when any field qualifier is present. `issue_fields:` raises
114
+ `IssueFieldsNotEnabledError` when `config.issue_fields_enabled` is `false` (closes #54).
115
+
116
+ ## 0.23.1
117
+
118
+ ### Added
119
+
120
+ - `PlanMyStuff::UserResolver.from_github_login(login)` - inverse of `config.github_login_for`. Returns the
121
+ resolved user object whose configured GitHub login matches `login`, or `nil` when `login` is blank or
122
+ unmapped.
123
+
124
+ ### Changed
125
+
126
+ - `Webhooks::GithubController` now forwards the actor through to `Pipeline.take!`'s `user:` kwarg from
127
+ `handle_issue_assigned` (using `assignee.login`) and `handle_draft_opened` (using the PR author). The
128
+ resolved user lands on the `pipeline_started.plan_my_stuff` notification payload.
129
+ - `Issues::TakesController#create` now forwards `pms_current_user` to `Pipeline.take!`'s `user:` kwarg.
130
+
131
+ ## 0.23.0
132
+
133
+ ### Added
134
+
135
+ - `PlanMyStuff::Pipeline.take!` and `PlanMyStuff::Pipeline.request_testing!` now accept a `user:` kwarg
136
+ that is forwarded onto the `pipeline_started.plan_my_stuff` / `pipeline_testing.plan_my_stuff`
137
+ `ActiveSupport::Notifications` payload. Defaults to `nil`, falling back to
138
+ `config.current_user` via `PlanMyStuff::Notifications.resolve_current_user`.
139
+
140
+ ## 0.22.0
141
+
142
+ ### Added
143
+
144
+ - `Issue#add_to_priority_list!(priority:)` and `Issue#remove_from_priority_list!` - first-class setters that
145
+ mirror the `#priority_list?` / `#priority_list_priority` readers. Each writes both `Priority List` and
146
+ `Priority List Priority` in a single mutation so the two fields cannot drift apart (closes #80).
147
+
148
+ ### Changed
149
+
150
+ - `IssueMetadata#priority_list` / `#priority_list_priority` deprecation message now points callers at the new
151
+ `Issue#add_to_priority_list!` / `#remove_from_priority_list!` pair instead of raw `set_issue_fields!`.
152
+
153
+ ## 0.21.1
154
+
155
+ ### Added
156
+
157
+ - `config.eager_load_controllers_on_boot` (default `false`). When `true`, the engine eager-loads
158
+ `app/controllers/` in `after_initialize` so consuming apps with `eager_load = false` (dev mode) see
159
+ `PlanMyStuff::*Controller` constants without having to reference them first. Opt in if the host app
160
+ probes engine controllers via `defined?`, e.g. `defined?(PlanMyStuff::Issues::TakesController)` would
161
+ otherwise return `nil` until something triggered Zeitwerk autoload. Falls back to `Kernel#require` on
162
+ Zeitwerk < 2.6.2 (which lacks `eager_load_dir`).
163
+
164
+ ## 0.21.0
165
+
166
+ ### Breaking
167
+
168
+ - `PMS::Attachment` no longer accepts/stores a single `url:` attribute. Construct with explicit
169
+ `filename:`, `owner:`, `repo:`, `sha:`, `path:` instead; `#url` is now a derived method that returns the
170
+ GitHub blob viewer URL (`https://github.com/{owner}/{repo}/blob/{sha}/{path}`) rather than the
171
+ `raw.githubusercontent.com` URL. `#to_h` emits the structured fields and no longer includes a `url` key.
172
+ Persisted comment metadata in the legacy `{filename, url}` shape is migrated on read and rewritten in the
173
+ new shape on next save (closes #70).
174
+
175
+ ### Added
176
+
177
+ - Comment bodies now render a visible `<details><summary>attachments (N)</summary>` block listing each
178
+ attachment as a markdown link to its blob URL, between the `pms-metadata` block and the user body. The
179
+ parser strips this block on read so round-trips stay clean.
180
+
181
+ ## 0.20.0
182
+
183
+ ### Breaking
184
+
185
+ - `priority_list` and `priority_list_priority` keys removed from `IssueMetadata` serialization. Existing issues
186
+ migrate to the GitHub Issue Field equivalents (`Priority List`, `Priority List Priority`) on their next save.
187
+
188
+ ### Added
189
+
190
+ - `Issue#priority_list?` and `Issue#priority_list_priority` read the corresponding issue field values directly.
191
+ - `Issue.priority_list(repo:, state:, labels:, page:, per_page:)` convenience method delegates to
192
+ `Issue.list(priority_list: true, ...)`.
193
+ - `Issue.list` and `Issue.count` accept `priority_list:` for server-side filtering via the GitHub issue fields
194
+ API (passes `priority_list: false` raises `ArgumentError` -- the API has no negation qualifier).
195
+
196
+ ### Deprecated
197
+
198
+ - `IssueMetadata#priority_list`, `#priority_list=`, `#priority_list?`, `#priority_list_priority`, and
199
+ `#priority_list_priority=` emit deprecation warnings on each read/write. Legacy writes forward to the issue
200
+ field on `Issue#save!` / `#update!` / `Issue.create!`. The accessors will be removed in 1.0.0.
201
+
202
+ ## 0.19.0
203
+
204
+ ### Added
205
+
206
+ - Every controller action now yields its primary object to a block when subclassed, so consumers can do
207
+ `super do |obj| ... end` to wedge in audit-log / instrumentation without rewriting the action. Webhook
208
+ controllers yield `(payload, result)`. See [CONFIGURATION.md](CONFIGURATION.md).
209
+
210
+ ## 0.18.0
211
+
212
+ ### Breaking
213
+
214
+ - `PMS::Issue#to_param` now returns `"Nickname-number"` (e.g. `"Rawr-1234"`) instead of the default ActiveModel id.
215
+ Single-issue URLs change from `/issues/1234?repo=rawr` to `/issues/Rawr-1234`. Consuming apps using
216
+ `youtrack_issue_path(@issue)` work natively; any hand-rolled URLs that passed `@issue.number` plus a `repo:`
217
+ query param must switch to `@issue` (or `@issue.to_param`). The mounted engine's controllers and views have
218
+ been updated to the new shape, as has the markdown link embedded in the GitHub issue body via
219
+ `Issue#user_link`.
220
+
221
+ ### Added
222
+
223
+ - `config.repo_nicknames` (default `{}`) -- Symbol-keyed map from repo key to the human-readable label used as
224
+ the `Issue#to_param` prefix. Missing keys fall back to `key.to_s.titleize`, so `:rawr` -> `"Rawr"` is free;
225
+ only divergent ones (e.g. `safety: 'Compliance'`) need an entry.
226
+ - `Configuration#repo_nickname_for(key)` accessor, `Repo#nickname`, and `Repo.from_nickname!` for the inverse
227
+ lookup used by `Issue.from_param`.
228
+ - `Issue#to_param` and `Issue.from_param` -- the latter parses `"Nickname-1234"` back into `[Repo, Integer]`.
229
+ - `Issue.find` first arg now accepts a nickname-id String (e.g. `"Rawr-1234"`) in addition to Integer / digit-
230
+ String + `repo:` kwarg. The nickname form ignores `repo:`.
231
+
232
+ ## 0.17.0
233
+
234
+ ### Added
235
+
236
+ - `PlanMyStuff.deprecator` returns a gem-scoped `ActiveSupport::Deprecation` instance.
237
+ Consuming apps can silence, filter, or re-route PMS deprecation warnings independently
238
+ of Rails' own deprecator.
239
+
240
+ ### Changed
241
+
242
+ - Issue, comment, and project-readme metadata is now stored in a visible `<details>` block
243
+ with a JSON code fence instead of a hidden HTML comment. GitHub renders the block in-page
244
+ so the metadata is inspectable without opening the body editor (closes #58).
245
+ `MetadataParser.parse` still accepts the legacy `<!-- pms-metadata:... -->` format and
246
+ emits a deprecation warning when it sees one; pre-0.17.0 bodies keep parsing and
247
+ re-serialize to the new format on their next write. Legacy detection will be removed
248
+ in 1.0.0.
249
+
250
+ ## 0.16.0
251
+
252
+ ### Added
253
+
254
+ - `PlanMyStuff::Issue.count(repo:, state:, labels:)` returns the total count of matching issues via the
255
+ GitHub Search API in a single request, without paginating full issue payloads. Mirrors `Issue.list`
256
+ kwargs (minus `page`/`per_page`); returns an `Integer`. Excludes PRs server-side via the `is:issue`
257
+ qualifier.
258
+
259
+ ## 0.15.0
260
+
261
+ ### Breaking
262
+
263
+ - `ActiveSupport::Notifications` events renamed to follow Rails' convention (event first, library
264
+ last). Subscribers matching the old `plan_my_stuff.*` prefix must switch to `*.plan_my_stuff`.
265
+
266
+ | Old | New |
267
+ |----------------------------------------------|----------------------------------------------|
268
+ | `plan_my_stuff.issue.created` | `issue_created.plan_my_stuff` |
269
+ | `plan_my_stuff.issue.updated` | `issue_updated.plan_my_stuff` |
270
+ | `plan_my_stuff.issue.closed` | `issue_closed.plan_my_stuff` |
271
+ | `plan_my_stuff.issue.reopened` | `issue_reopened.plan_my_stuff` |
272
+ | `plan_my_stuff.issue.archived` | `issue_archived.plan_my_stuff` |
273
+ | `plan_my_stuff.issue.closed_inactive` | `issue_closed_inactive.plan_my_stuff` |
274
+ | `plan_my_stuff.issue.reopened_by_reply` | `issue_reopened_by_reply.plan_my_stuff` |
275
+ | `plan_my_stuff.issue.marked_duplicate` | `issue_marked_duplicate.plan_my_stuff` |
276
+ | `plan_my_stuff.issue.viewers_added` | `issue_viewers_added.plan_my_stuff` |
277
+ | `plan_my_stuff.issue.viewers_removed` | `issue_viewers_removed.plan_my_stuff` |
278
+ | `plan_my_stuff.issue.approval_requested` | `issue_approval_requested.plan_my_stuff` |
279
+ | `plan_my_stuff.issue.approval_granted` | `issue_approval_granted.plan_my_stuff` |
280
+ | `plan_my_stuff.issue.approval_revoked` | `issue_approval_revoked.plan_my_stuff` |
281
+ | `plan_my_stuff.issue.approval_rejected` | `issue_approval_rejected.plan_my_stuff` |
282
+ | `plan_my_stuff.issue.rejection_revoked` | `issue_rejection_revoked.plan_my_stuff` |
283
+ | `plan_my_stuff.issue.all_approved` | `issue_all_approved.plan_my_stuff` |
284
+ | `plan_my_stuff.issue.approvals_invalidated` | `issue_approvals_invalidated.plan_my_stuff` |
285
+ | `plan_my_stuff.issue.reminder_due` | `issue_reminder_due.plan_my_stuff` |
286
+ | `plan_my_stuff.issue.link_reciprocal_failed` | `issue_link_reciprocal_failed.plan_my_stuff` |
287
+ | `plan_my_stuff.comment.created` | `comment_created.plan_my_stuff` |
288
+ | `plan_my_stuff.comment.updated` | `comment_updated.plan_my_stuff` |
289
+ | `plan_my_stuff.label.added` | `label_added.plan_my_stuff` |
290
+ | `plan_my_stuff.label.removed` | `label_removed.plan_my_stuff` |
291
+ | `plan_my_stuff.project_item.added` | `project_item_added.plan_my_stuff` |
292
+ | `plan_my_stuff.project_item.removed` | `project_item_removed.plan_my_stuff` |
293
+ | `plan_my_stuff.project_item.assigned` | `project_item_assigned.plan_my_stuff` |
294
+ | `plan_my_stuff.project_item.status_changed` | `project_item_status_changed.plan_my_stuff` |
295
+ | `plan_my_stuff.pipeline.<status>` | `pipeline_<status>.plan_my_stuff` |
296
+
297
+ Rationale: aligns with `sql.active_record`, `process_action.action_controller`, etc. and lets
298
+ subscribers match "everything from PMS" via the standard suffix-regex (`/\.plan_my_stuff$/`).
299
+
300
+ ## 0.14.0
301
+
302
+ ### Added
303
+
304
+ - `Repo.resolve!` now treats a String matching a configured repo key (e.g. `"dummy"` when
305
+ `config.repos[:dummy]` is set) as that key, returning a Repo with `key:` populated.
306
+ Previously only Symbols hit the key lookup; strings always went through `"Org/Repo"`
307
+ parsing and raised on names without a slash. This prevents the need to call `to_sym` from the consuming app
308
+
309
+ ## 0.13.0
310
+
311
+ ### Changed
312
+
313
+ - Internal: gem now autoloads `lib/plan_my_stuff/**` via Zeitwerk
314
+ (already shipped by railties - no new dep). All `require_relative`
315
+ boilerplate in the gem's lib tree is gone. The gem entry retains two
316
+ explicit `require_relative`s for files that fall outside the autoload
317
+ model: `errors.rb` (defines several sibling error classes) and
318
+ `engine.rb` (must register with Rails at load time).
319
+
320
+ ## 0.12.0
321
+
322
+ ### Added
323
+
324
+ - `PlanMyStuff::IssueField` for org-level GitHub Issue Field definitions (public preview).
325
+ Exposes `.list`, `.find` (case-insensitive), and `#option_id_for!` for resolving
326
+ single-select option names.
327
+ - `PlanMyStuff::Issue#issue_fields` returns a hash-like `IssueFieldValueSet` view of the
328
+ field values on an issue. Values are coerced to native types (`Date` for date fields,
329
+ `Float` for numbers, the option name `String` for single-selects).
330
+ - `PlanMyStuff::Issue#set_issue_fields!(updates)` writes one or more field values in a
331
+ single GraphQL mutation; passing `nil` as a value clears the field.
332
+ - `issue_fields:` kwarg on `Issue.create!`, `Issue.update!`, `Issue#save!`, and
333
+ `Issue#update!` so callers can set field values inline with create/update instead of
334
+ following up with an explicit `set_issue_fields!` call.
335
+ - `Configuration#issue_fields_enabled` (default `true`, opt-out). Set to `false` if your
336
+ org has not been admitted to the Issue Fields preview - with the flag off,
337
+ `Issue#issue_fields` returns an empty set and the write paths raise
338
+ `IssueFieldsNotEnabledError` instead of letting a raw GraphQL error surface.
339
+
340
+ ## 0.11.0
341
+
342
+ ### Added
343
+
344
+ - `PlanMyStuff::Attachment` value object (`filename`, `url`) and
345
+ `CommentMetadata#attachments`. PMS now owns attachment storage:
346
+ `Comment.create!` accepts an `attachments:` kwarg of uploaded-file
347
+ objects responding to `#path` and `#original_filename` (e.g.
348
+ Rails `ActionDispatch::Http::UploadedFile`), `String`/`Pathname`
349
+ paths to local files, or pre-built `PlanMyStuff::Attachment`
350
+ instances. Each file is committed to the shared attachment repo
351
+ (`config.attachment_repo`, default `'pms-attachments'`, under
352
+ `config.organization`) on `config.main_branch` at
353
+ `<repo_key>/issue-<N>/<uuid>.<ext>`; the resulting SHA-pinned
354
+ `raw.githubusercontent.com` permalink is stored in metadata as a
355
+ `PlanMyStuff::Attachment(filename:, url:)`. The attachment repo
356
+ must exist; the uploader does not create it. Malformed metadata
357
+ entries are silently dropped on parse, matching existing `links`
358
+ / `approvals` behavior on `IssueMetadata`.
359
+ - `config.attachment_repo` (default `'pms-attachments'`) names the
360
+ bare repo under `config.organization` that stores uploaded
361
+ attachment binaries.
362
+ - `PlanMyStuff::AttachmentUploader.upload_all!(repo:,
363
+ issue_number:, files:)` is the underlying uploader; called
364
+ internally by `Comment.create!` but also usable directly by apps
365
+ that want to upload before constructing a comment. Pre-built
366
+ `Attachment` instances pass through without re-uploading.
367
+ - `Issue.create!` accepts an `attachments:` kwarg, forwarded to the
368
+ body-comment `Comment.create!` call so attachments posted with a
369
+ new issue are recorded on (and uploaded for) the body comment.
370
+ - `PlanMyStuff::Attachment#download_to(path = nil)` fetches the file
371
+ via the GitHub Contents API (so it works on private repos) and
372
+ writes the bytes to disk. Defaults `path` to
373
+ `File.join(Dir.tmpdir, filename)` and returns the path written to.
374
+
375
+ ### Documented
376
+
377
+ - `designs/g9/` documents the design pivot from inline-base64 (body
378
+ size capped) through sibling-repo (rejected) to the chosen
379
+ side-branch approach.
380
+
381
+ ## 0.10.5
382
+
383
+ ### Added
384
+
385
+ - `config.parent_controller` (default `'::ApplicationController'`) lets consuming apps that don't use the
386
+ `ApplicationController` name (e.g. `ActionController::Base`, `RawrApplicationController`) tell the engine
387
+ what to inherit from. The `plan_my_stuff:verify` rake task now reports whether the configured class
388
+ resolves.
389
+
390
+ ### Fixed
391
+
392
+ - Example config option showed `url_options = Rails.application.config.action_mailer.default_url_options`. Now it shows
393
+ `url_options = Rails.application.routes.default_url_options`
394
+
395
+ ## 0.10.4
396
+
397
+ ### Changed
398
+
399
+ - Internal: unit specs now use VCR cassettes against `BrandsInsurance/ubiquitous-adventure` instead of
400
+ `instance_double(Octokit::Client)`. No effect on consuming apps.
401
+
402
+ ### Removed
403
+
404
+ - ETag caching on `Issue.list`. The cache key included `state`, `labels`, `page`, and `per_page`,
405
+ so an etag stored under one combo never matched a request with different params - the cache wrote
406
+ on every call and effectively never hit. `Issue.list` now always issues a fresh GitHub request.
407
+ `Comment.list` keeps ETag caching (param surface is just `issue` + `pms_only`).
408
+ `PlanMyStuff::Cache.read_list` / `write_list` remain available for `Comment.list`.
409
+
410
+ ## 0.10.3
411
+
412
+ ### Added
413
+
414
+ - `BaseProjectItem#assignees` returns the GitHub logins of users assigned to the underlying Issue,
415
+ PullRequest, or DraftIssue. Populated from the `FIND_PROJECT` GraphQL response (the query was extended
416
+ with `assignees(first: 10)` on each content fragment).
417
+
418
+ ## 0.10.2
419
+
420
+ ### Changed
421
+
422
+ - Nothing, just a version bump
423
+
424
+ ## 0.10.1
425
+
426
+ ### Changed
427
+
428
+ - Nothing, just a version bump
429
+
430
+ ## 0.10.0
431
+
432
+ ### Changed
433
+
434
+ - `Issue` slimmed from 1791 to 911 lines by extracting feature clusters into per-feature modules under a sibling
435
+ `PlanMyStuff::IssueExtractions::*` namespace, included into `Issue`. Public API unchanged (`issue.approve!`,
436
+ `issue.add_related!`, `issue.enter_waiting_on_user!`, `issue.add_viewers!`, etc. still resolve to the same methods).
437
+ - `PlanMyStuff::IssueExtractions::Approvals` - `lib/plan_my_stuff/issue_extractions/approvals.rb`
438
+ - `PlanMyStuff::IssueExtractions::Links` - `lib/plan_my_stuff/issue_extractions/links.rb`
439
+ - `PlanMyStuff::IssueExtractions::Waiting` - `lib/plan_my_stuff/issue_extractions/waiting.rb`
440
+ - `PlanMyStuff::IssueExtractions::Viewers` - `lib/plan_my_stuff/issue_extractions/viewers.rb`
441
+ - `BaseProject` slimmed from 661 to 504 lines by extracting GraphQL hydration helpers into
442
+ `PlanMyStuff::BaseProjectExtractions::GraphqlHydration` (`lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb`),
443
+ included into `BaseProject`'s singleton class. `BaseProject.find` / `BaseProject.list` remain public entry points.
444
+ - Specs for `Issue` features now live alongside the modules at `spec/plan_my_stuff/issue_extractions/<feature>_spec.rb`.
445
+
446
+ ## 0.9.0
447
+
448
+ ### Breaking
449
+
450
+ - `config.should_send_request` and `config.job_classes` accessors — declared but never wired up. The request gateway that would honor them is deferred (see `requirements/09_request_gateway.md` and `designs/init/gem_mvp_deferred_notes.md`); they will return when the gateway lands
451
+
452
+ ### Added
453
+
454
+ - `CONFIGURATION.md` is now available bundled with the gem
455
+
456
+ ### Changed
457
+
458
+ - `config.repos` is now assignable as a whole hash (`config.repos = { atlas: 'Org/Atlas', autofill: 'Org/atlas-autofill-moz' }`) in addition to the existing `config.repos[:key] = '...'` form
459
+ - `Issue.create!` / `Issue.update!` `issue_type:` kwarg now accepts the Symbol nicknames (`'bug'`, `'feature'`, `'it_issue'`, `'other'`, `'performance'`, `'question'`, `'task'`) as Strings too, resolving them to the same canonical name
460
+
461
+ ## 0.8.0
462
+
463
+ ### Breaking
464
+
465
+ - Issue show "Take" button is now hidden when the issue already has assignees, and `Issues::TakesController#create` rejects the request with a flash error naming the existing assignee. Best-effort race guard for two users clicking Take simultaneously — without it, GitHub silently piles the second user on as a co-assignee.
466
+ - Guard lives in the controller, not `Pipeline.take!` itself, because webhook paths (`handle_issue_assigned`, `handle_projects_v2_item`, `handle_draft_opened`, `handle_converted_to_draft`) legitimately call `take!` on already-assigned issues
467
+ - `Issue#add_viewers` / `Issue#remove_viewers` renamed to `Issue#add_viewers!` / `Issue#remove_viewers!` for consistency with other mutating instance methods (`approve!`, `close!`, etc.)
468
+ - `Approval#status` extended to a 3-state model (`pending`, `approved`, `rejected`); `Approval` gains a `rejected_at` attribute serialized in `to_h` and the metadata blob. Existing approvals deserialize unchanged (`rejected_at` defaults to `nil`)
469
+ - `Issue#fully_approved?` now requires every approver to be `approved` — a single rejection blocks the gate until revoked. Previously equivalent to `pending_approvals.empty?`, which silently treated rejections as "done"
470
+ - `Issue#revoke_approval!` accepts either an `approved` or `rejected` source state (previously only `approved`); raises `ValidationError` when the target is still `pending`. Authorization error message updated to "another user's response"
471
+
472
+ ### Added
473
+
474
+ - `CONFIGURATION.md` documents every `PlanMyStuff::Configuration` option grouped by concern; `README.md` Configuration section trimmed to the two required options plus a link
475
+ - Install generator's initializer template now includes the four pipeline options previously missing from it: `pipeline_testing_field_name`, `pipeline_testing_values`, `pipeline_completion_purge_enabled`, `pipeline_completion_ttl_hours`
476
+ - Project show "Remove from project" button (and `DELETE /projects/:project_id/items/:id` route on `ProjectItemsController`) lets the user remove an item from a project board without bouncing out to GitHub. Calls `ProjectItem#destroy!` which fires `plan_my_stuff.project_item.removed`
477
+ - Issue show "Release" button (and `DELETE /issues/:issue_id/take` route on `Issues::TakesController`) lets a dev who took an issue undo their assignment from the same view, without bouncing out to GitHub or the project board. When the current user is the sole assignee the issue's GitHub assignees are cleared and the project item is removed from the pipeline (`project_item.destroy!`); when other assignees remain the current user is unassigned via `project_item.assign!(remaining)` and the item stays on the project
478
+ - `config.import_access_token` classic PAT (requires `repo` scope) used exclusively for the Issues Import API (`golden-comet-preview`); fine-grained tokens are not supported by that endpoint. Optional — defaults to `nil`
479
+ - `Issue.import!(payloads)` POSTs an `Array<Hash>` (one POST per payload) to GitHub's "Import Issues" preview endpoint and returns one status hash per input. Each payload must include `:repo` plus the GitHub-shaped `:issue` / `:comments` keys; payloads are passed through unchanged otherwise
480
+ - `Issue.check_import!(import_id, repo:)` polls a previously-submitted import for its status
481
+ - `Issue#created_at` / `Comment#created_at` attributes hydrated from GitHub's `created_at` field
482
+ - `Issue#issue_type` reader hydrated from GitHub's native `type.name`. `Issue.create!` and `Issue.update!` accept an `issue_type:` kwarg as a String (passed through), Symbol shortcut (`:bug`, `:feature`, `:it_issue`, `:other`, `:performance`, `:question`, `:task`), or `nil`. On `update!`, omitting the kwarg leaves the type untouched; passing `nil` clears it
483
+ - `config.issue_types` Hash{String => String} maps canonical type names to org-specific display names so consuming apps can rename without touching call sites (e.g. `{ 'Feature' => 'Enhancement' }`); missing keys pass through unchanged
484
+ - `config.controller_rescue` Proc invoked from every user-facing controller `rescue` block (after the gem logs the error and stack trace) so consuming apps can forward swallowed errors to their monitoring service
485
+ - `Issue#reject!(user:)` symmetric with `approve!`; accepts either `pending` or `approved` source state and raises `ValidationError` when already rejected. Fires `plan_my_stuff.issue.approval_rejected` and, when the flip drops the issue out of `fully_approved?`, `approvals_invalidated(trigger: :rejected)`
486
+ - `Issue#approve!` now accepts either `pending` or `rejected` source state; raises `ValidationError` only on `approved -> approved`. Clears `rejected_at` on the rejected -> approved transition
487
+ - `Issue#rejected_approvals` reader returns the subset of approvers with `status == 'rejected'`
488
+ - New events: `plan_my_stuff.issue.approval_rejected` and `plan_my_stuff.issue.rejection_revoked` (the latter fires when revoking a rejection back to pending)
489
+ - Approvals partial renders a `Reject` button alongside `Approve` for pending approvers, a `rejected at <ts>` row, a parenthetical rejected count in the header, and a `Revoke` button for either non-pending state
490
+
491
+ ## 0.7.0
492
+
493
+ ### Added
494
+
495
+ - `Issue#user_link` returns the per-issue URL in the consuming app, computed from `config.issues_url_prefix` + issue number
496
+
497
+ ### Changed
498
+
499
+ - GitHub issues created/updated by PMS now render a markdown link `[Org/Repo#number](user_link)` as the visible body (previously empty); skipped when `issues_url_prefix` is unset
500
+
501
+ ## 0.6.0
502
+
503
+ ### Breaking
504
+
505
+ - `Pipeline.submit!` removed; consuming apps that called it directly should switch to `ProjectItem.create!` + `Pipeline.take!`
506
+ - `Pipeline::Status::SUBMITTED` and `Pipeline::Status::TESTING` constants removed; `Status::ALL` no longer includes them
507
+ - `Pipeline.request_testing!` no longer moves the `Status` field — it now writes to a separate `Testing` single-select custom field on the pipeline project. The pipeline project must have a `Testing` field with `Testing` / `Not testing` options
508
+ - Active pipeline status set is now `Started -> In Review -> Ready for Release -> Release in Progress -> Completed`
509
+
510
+ ### Added
511
+
512
+ - `Pipeline::Testing` module (`FIELD_NAME`, `VALUES`)
513
+ - `Pipeline::CompletedSweep` removes items from `Completed` after the configured TTL; driven by `RemindersSweepJob`
514
+ - `BaseProjectItem#update_single_select_field!` instance method
515
+ - `BaseProjectItem#updated_at` populated from GitHub's `updatedAt`
516
+ - `Issue#assignees` public reader
517
+ - `Pipeline.release_cycle_locked?` helper
518
+ - New config: `pipeline_testing_field_name`, `pipeline_testing_values`, `pipeline_completion_purge_enabled` (default `true`), `pipeline_completion_ttl_hours` (default `24`)
519
+
520
+ ### Changed
521
+
522
+ - Assigning a dev to an issue now creates the project item and lands it at `Started` (was `Submitted`)
523
+ - Opening a PR as a draft adds the linked issue to the pipeline at `Started` and assigns the PR author when the issue has no assignees; already-pipelined items are not moved
524
+ - The assignment webhook no longer calls `assign!` on the project item — GitHub already records the assignment, and `assign!` could clobber co-assignees
525
+
526
+ ### Removed
527
+
528
+ - `Pipeline.submit!` (see Breaking)
529
+ - `Pipeline::Status::SUBMITTED`, `Pipeline::Status::TESTING` (see Breaking)
530
+
531
+ ### Fixed
532
+
533
+ - Closed issues no longer alter the pipeline on assign/unassign webhook events
534
+ - Items at `Ready for Release`, `Release in Progress`, or `Completed` are no longer removed when an unassign webhook fires
535
+ - Pending-approvals guard runs before `ProjectItem.create!` in the assignment flow so the webhook no longer leaves orphan items when `Pipeline.take!`'s approval guard would otherwise raise
536
+
537
+ ### Documented
538
+
539
+ - `designs/pipeline_cleanup/plan.md` — full design + bug-to-fix mapping
540
+ - `requirements/08_release_pipeline.md` updated to reflect the new flow
541
+ - `requirements/tasks.md` — T-PC-001 added under Phase 14
542
+
543
+ ## 0.5.1
544
+
545
+ ### Fixed
546
+
547
+ - AWS webhook rejected SNS `SubscriptionConfirmation` and `UnsubscribeConfirmation` messages with 401; now signature-verified, logged, and acked with 200
548
+
549
+ ## 0.5.0
550
+
551
+ ### Added
552
+
553
+ - `post_install_message` in gemspec listing available rake tasks and generators
554
+ - `rake plan_my_stuff:install` and `rake plan_my_stuff:views` aliases for the Rails generators
555
+
556
+ ### Fixed
557
+
558
+ - `TestingProjectItem.create!` left new items with a blank Test Status; now defaults to `Todo`
559
+ - `TestingProjectItem.create!` left new items with no Pass Mode; now defaults to `all`
560
+
561
+ ## 0.4.0
562
+
563
+ - Full feature set release before PR
564
+
565
+ ## 0.3.0
566
+
567
+ ### Added
568
+
569
+ - Add `new`/`create`/`edit`/`update` routes for Projects
570
+
571
+ ## 0.2.0
572
+
573
+ ### Added
574
+
575
+ - Validate `config.custom_fields` on `create!`/`update!` actions
576
+
577
+ ### Changes
578
+
579
+ - CodeRabbit improvements
580
+
581
+ ## 0.1.2
582
+
583
+ ### Fixed
584
+
585
+ - Issues' links were looking for the issue in the default repo with the same number
586
+
587
+ ## 0.1.1
588
+
589
+ ### Fixed
590
+
591
+ - Wrong changelog included in publish
592
+
593
+ ## 0.1.0
594
+
595
+ - Initial Setup