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
data/CONFIGURATION.md ADDED
@@ -0,0 +1,487 @@
1
+ # Configuration
2
+
3
+ Every PlanMyStuff option lives on `PlanMyStuff::Configuration`. The install generator drops a fully-commented copy at
4
+ `config/initializers/plan_my_stuff.rb` — this file documents the same options grouped by concern. Defaults shown apply
5
+ when the option is left unset.
6
+
7
+ ```ruby
8
+ PMS.configure do |config|
9
+ # ...
10
+ end
11
+ ```
12
+
13
+ `PMS` is an alias for `PlanMyStuff`; consuming apps can use either.
14
+
15
+ ## Authentication (required)
16
+
17
+ | Option | Type | Default | Description |
18
+ |---|---|---|---|
19
+ | `access_token` | `String` | — | GitHub PAT with `repo` and `project` scopes. Required. |
20
+ | `import_access_token` | `String, nil` | `nil` | Classic PAT (requires `repo` scope) for the Issues Import API. Fine-grained tokens are not supported by that endpoint. |
21
+ | `organization` | `String` | — | GitHub organization name. Required. |
22
+
23
+ Both `access_token` and `organization` are validated by `config.validate!`; missing or blank values raise `PlanMyStuff::ConfigurationError`.
24
+
25
+ > [!NOTE]
26
+ > `import_access_token` is only required if you choose to use `PMS::Issue.import!`/`PMS::Issue.check_import!`
27
+
28
+ ```ruby
29
+ config.access_token = Rails.application.credentials.dig(:plan_my_stuff, :github_token)
30
+ config.import_access_token = Rails.application.credentials.dig(:plan_my_stuff, :github_import_token)
31
+ config.organization = 'YourOrg'
32
+ ```
33
+
34
+ ## Repositories
35
+
36
+ | Option | Type | Default | Description |
37
+ |---|---|---|---|
38
+ | `repos` | `Hash{Symbol => String}` | `{}` | Named repo configs mapping a key to an `Org/Repo` string. |
39
+ | `default_repo` | `Symbol, nil` | `nil` | Repo key used when callers omit the `repo:` param. |
40
+ | `repo_nicknames` | `Hash{Symbol => String}` | `{}` | `Issue#to_param` prefix override (default `key.titleize`). |
41
+
42
+ ```ruby
43
+ config.repos = { element: 'YourOrg/Element', underwriter: 'YourOrg/Underwriter' }
44
+ config.default_repo = :element
45
+ config.repo_nicknames = { safety: 'Compliance' } # :element -> "Element", :underwriter -> "Underwriter" come free
46
+ ```
47
+
48
+ `repos` can be mutated via `config.repos[:key] = '...'` or set via `config.repos = { key: 'MyOrg/MyRepo' }`.
49
+ `Issue#to_param` then returns `"Element-1234"` / `"Compliance-567"`, encoding both repo and number in a single
50
+ URL segment so `youtrack_issue_path(@issue)` works without a `repo:` query param.
51
+
52
+ ## Attachments
53
+
54
+ | Option | Type | Default | Description |
55
+ |---|---|---|---|
56
+ | `attachment_repo` | `String` | `'pms-attachments'` | Bare repo (under `organization`) for uploaded attachments. |
57
+
58
+ The repo must already exist; the uploader does not create it. Attachments commit onto
59
+ `config.main_branch` under `<repo_key_or_name>/issue-<number>/<uuid>.<ext>`.
60
+
61
+ ```ruby
62
+ config.attachment_repo = 'pms-attachments'
63
+ ```
64
+
65
+ ## Projects
66
+
67
+ | Option | Type | Default | Description |
68
+ |---|---|---|---|
69
+ | `default_project_number` | `Integer, nil` | `nil` | Default Projects V2 number for `add_to_project: true`. |
70
+ | `testing_template_project_number` | `Integer, nil` | `nil` | Project to clone in `TestingProject.create!` instead of bootstrapping fields. |
71
+
72
+ ```ruby
73
+ config.default_project_number = 14
74
+ config.testing_template_project_number = 42
75
+ ```
76
+
77
+ ## App identity
78
+
79
+ | Option | Type | Default | Description |
80
+ |---|---|---|---|
81
+ | `app_name` | `String, nil` | `nil` | Stored in metadata so subscribers can attribute writes. |
82
+ | `issues_url_prefix` | `String, nil` | `nil` | Prefix for `Issue#user_link`; the gem appends the issue number. |
83
+
84
+ ```ruby
85
+ config.app_name = 'MyApp'
86
+
87
+ url_options = Rails.application.routes.default_url_options
88
+ config.issues_url_prefix = "#{url_options[:protocol] || 'http'}://#{url_options[:host]}/issues"
89
+ ```
90
+
91
+ ## User integration
92
+
93
+ | Option | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `user_class` | `String` | `'User'` | Consuming app's user model class name, constantized for lookups. |
96
+ | `display_name_method` | `Symbol` | `:to_s` | Method called on a user to get the display name for comment headers. |
97
+ | `user_id_method` | `Symbol` | `:id` | Method called on a user to extract the app-side user ID. |
98
+ | `support_method` | `Symbol, Proc` | `:support?` | Method name on the user, or a proc receiving the user, returning whether they're support staff. |
99
+ | `github_login_for` | `Hash{Object => String}` | `{}` | Maps app user id (from `user_id_method`) to GitHub login. Powers the Take UI. |
100
+
101
+ `support_method` may be a method name on the user object or a proc that receives the user and returns boolean.
102
+ `github_login_for` maps app user id (whatever `user_id_method` returns) to GitHub login and powers the Take UI.
103
+
104
+ ```ruby
105
+ config.user_class = 'User'
106
+ config.display_name_method = :full_name
107
+ config.user_id_method = :id
108
+ config.support_method = :support?
109
+ # or: config.support_method = -> (user) { user.role.in?(%w[support admin]) }
110
+ config.github_login_for = {
111
+ 1 => 'some_username',
112
+ 2 => 'octocat',
113
+ }
114
+ ```
115
+
116
+ ## Engine authentication
117
+
118
+ | Option | Type | Default | Description |
119
+ |---|---|---|---|
120
+ | `authenticate_with` | `Proc` | `nil` | Block executed as a `before_action` on every engine controller. |
121
+
122
+ ```ruby
123
+ config.authenticate_with do
124
+ redirect_to main_app.login_path unless current_user
125
+ end
126
+ ```
127
+
128
+ ## Markdown rendering
129
+
130
+ | Option | Type | Default | Description |
131
+ |---|---|---|---|
132
+ | `markdown_renderer` | `Symbol` | `:commonmarker` | Which markdown gem to use: `:commonmarker`, `:redcarpet`, or `nil` (raw HTML-escaped). |
133
+ | `markdown_options` | `Hash` | `{}` | Default options passed to the renderer. Per-call options merge on top of these. |
134
+
135
+ Set `markdown_renderer` to `:commonmarker`, `:redcarpet`, or `nil` (raw HTML-escaped). The chosen gem must be in your
136
+ Gemfile.
137
+
138
+ ```ruby
139
+ config.markdown_renderer = :commonmarker
140
+ config.markdown_options = { render: { hardbreaks: true } }
141
+ ```
142
+
143
+ ## Notification actor
144
+
145
+ | Option | Type | Default | Description |
146
+ |---|---|---|---|
147
+ | `current_user` | `Proc, nil` | `nil` | Fallback actor for notification events when `user:` is not passed. |
148
+
149
+ ```ruby
150
+ config.current_user = -> { Current.user }
151
+ ```
152
+
153
+ ## Controller rescue
154
+
155
+ | Option | Type | Default | Description |
156
+ |---|---|---|---|
157
+ | `controller_rescue` | `Proc, nil` | `nil` | Receives the rescued exception. Forward to your monitoring service. |
158
+
159
+ ```ruby
160
+ config.controller_rescue = -> (error) { MonitoringService.notice_error(error) }
161
+ ```
162
+
163
+ ## Custom fields
164
+
165
+ App-defined fields stored in metadata. Keys are field names; values are hashes with `:type` and `:required`.
166
+ Supported types: `:string`, `:integer`, `:boolean`, `:array`, `:hash`.
167
+
168
+ | Option | Type | Default | Description |
169
+ |---|---|---|---|
170
+ | `custom_fields` | `Hash{Symbol => Hash}` | `{}` | Shared field definitions across all contexts. |
171
+ | `issue_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Issue-only definitions; merged on top of shared. |
172
+ | `comment_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Comment-only definitions; merged on top of shared. |
173
+ | `project_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Project-only definitions; merged on top of shared. |
174
+ | `testing_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Testing-project-only definitions; merged on top of shared. |
175
+
176
+ Context-specific config wins on key conflicts.
177
+
178
+ ```ruby
179
+ config.custom_fields = {
180
+ notification_recipients: { type: :array, required: true },
181
+ }
182
+ config.issue_custom_fields = {
183
+ ticket_type: { type: :string, required: true },
184
+ }
185
+ config.comment_custom_fields = {
186
+ internal_note: { type: :boolean },
187
+ }
188
+ config.project_custom_fields = {
189
+ team: { type: :string },
190
+ }
191
+ config.testing_custom_fields = {
192
+ test_plan_url: { type: :string },
193
+ }
194
+ ```
195
+
196
+ ## Issue types
197
+
198
+ | Option | Type | Default | Description |
199
+ |---|---|---|---|
200
+ | `issue_types` | `Hash{String => String}` | `{}` | Maps the gem's canonical issue type names to your org's display names. |
201
+
202
+ Maps the gem's canonical type names (`'Bug'`, `'Feature'`, `'IT Issue / Hardware'`, `'Other'`, `'Performance'`,
203
+ `'Question'`, `'Task'`) to whatever your org uses. Missing keys pass through unchanged.
204
+
205
+ ```ruby
206
+ config.issue_types = {
207
+ 'Bug' => 'User Bug',
208
+ 'Feature' => 'Enhancement',
209
+ }
210
+ ```
211
+
212
+ ## Issue Fields (public preview)
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `issue_fields_enabled` | `Boolean` | `true` | Whether the Issue Fields public preview is wired up for the org. |
217
+ | `issue_field_names` | `Hash{String => String}` | `{}` | Canonical Issue Field name => the consumer org's field name. |
218
+ | `issue_field_values` | `Hash` | `{}` | Per field: canonical value => consumer value (single-select labels). |
219
+
220
+ GitHub Issue Fields are structured per-issue metadata (text, number, date, or single-select)
221
+ configured once at the org level. The preview is rolling out org-by-org. Leave this flag at its
222
+ default (`true`) once your org has been admitted; flip to `false` to keep the gem from issuing
223
+ calls that would otherwise return raw GraphQL errors.
224
+
225
+ With the flag off:
226
+
227
+ - `Issue#issue_fields` returns an empty `IssueFieldValueSet` without making a request.
228
+ - `Issue#set_issue_fields!(...)` and `IssueField.list` raise `IssueFieldsNotEnabledError`.
229
+
230
+ ```ruby
231
+ config.issue_fields_enabled = false # org not admitted to the preview yet
232
+ ```
233
+
234
+ `issue_field_names` and `issue_field_values` let a consuming org rename the native Issue Fields (and their
235
+ single-select option labels) the gem refers to internally, the same way `pipeline_statuses` aliases pipeline
236
+ statuses. Translation is bidirectional via `PlanMyStuff::IssueFieldTranslation`: canonical => consumer on writes and
237
+ filters, consumer => canonical on reads, so internal comparisons (`awaiting_reply?`, `priority_list?`) keep working.
238
+ Unmapped names / values pass through unchanged.
239
+
240
+ ```ruby
241
+ config.issue_field_names = {
242
+ 'Issue Status' => 'Status',
243
+ 'Priority' => 'Prio',
244
+ }
245
+ config.issue_field_values = {
246
+ 'Issue Status' => {
247
+ 'Submitted' => 'Triaged',
248
+ 'Waiting on Reply' => 'Awaiting Customer',
249
+ 'Open' => 'Open',
250
+ 'Reopened' => 'Reopened',
251
+ },
252
+ }
253
+ ```
254
+
255
+ ## Release pipeline
256
+
257
+ | Option | Type | Default | Description |
258
+ |---|---|---|---|
259
+ | `pipeline_enabled` | `Boolean` | `true` | Whether the release pipeline feature is enabled. |
260
+ | `pipeline_project_number` | `Integer, nil` | `nil` | Projects V2 number for the pipeline board. Falls back to `default_project_number`. |
261
+ | `webhook_secret` | `String, nil` | `nil` | HMAC secret for GitHub webhook signature verification. Required when webhook routes are mounted. |
262
+ | `pipeline_statuses` | `Hash{String => String}` | `{}` | Display aliases for canonical pipeline status names. |
263
+ | `pipeline_testing_field_name` | `String` | `'Testing'` | Display name for the pipeline project's `Testing` single-select field. |
264
+ | `pipeline_testing_values` | `Hash{Symbol => String}` | `{ active: 'Testing', inactive: 'Not testing' }` | Display labels for the canonical `:active`/`:inactive` testing options. |
265
+ | `pipeline_completion_purge_enabled` | `Boolean` | `true` | Whether the sweep removes aged-out `Completed` items from the pipeline. |
266
+ | `pipeline_completion_ttl_hours` | `Integer` | `24` | Hours after a `Completed` item's last update at which the sweep removes it. |
267
+ | `main_branch` | `String` | `'main'` | Branch PRs merge into for the "Ready for Release" transition. |
268
+ | `production_branch` | `String` | `'production'` | Branch PRs mereg into for the "Release in Progress" transition. |
269
+
270
+ `pipeline_statuses` aliases the canonical status names (`'Submitted'`, `'Started'`, `'In Review'`, `'Testing'`,
271
+ `'Ready for Release'`, `'Release in Progress'`, `'Completed'`) for display only. The constants remain the internal
272
+ identifiers.
273
+
274
+ `pipeline_completion_*` controls the sweep that removes aged-out `Completed` items from the pipeline. `webhook_secret`
275
+ is required when webhook routes are mounted.
276
+
277
+ ```ruby
278
+ config.pipeline_enabled = true
279
+ config.pipeline_project_number = 14
280
+ config.webhook_secret = Rails.application.credentials.dig(:plan_my_stuff, :webhook_secret)
281
+ config.pipeline_statuses = {
282
+ 'Submitted' => 'Triaged',
283
+ 'Completed' => 'Done',
284
+ }
285
+ config.pipeline_testing_field_name = 'Testing'
286
+ config.pipeline_testing_values = { active: 'Testing', inactive: 'Not testing' }
287
+ config.pipeline_completion_purge_enabled = true
288
+ config.pipeline_completion_ttl_hours = 24
289
+ config.main_branch = 'main'
290
+ config.production_branch = 'production'
291
+ ```
292
+
293
+ ## Follow-up reminders
294
+
295
+ | Option | Type | Default | Description |
296
+ |---|---|---|---|
297
+ | `reminders_enabled` | `Boolean` | `true` | Whether the reminders sweep performs any work. |
298
+ | `reminder_days` | `Array<Integer>` | `[1, 3, 7, 10, 14, 18]` | Days-since-waiting at which reminder events fire. |
299
+ | `inactivity_close_days` | `Integer` | `30` | Days of inactivity after which the sweep auto-closes a waiting issue. |
300
+ | `waiting_on_user_label` | `String` | `'waiting-on-user'` | Label flagging issues waiting on an end-user reply. |
301
+ | `waiting_on_approval_label` | `String` | `'waiting-on-approval'` | Label flagging issues waiting on pending approvals. |
302
+ | `user_inactive_label` | `String` | `'user-inactive'` | Label applied to issues auto-closed by the inactivity sweep; removed when an issue is auto-reopened via a user reply. |
303
+
304
+ Per-issue reminder override: `issue.metadata.reminder_days = [...]`.
305
+
306
+ ```ruby
307
+ config.reminders_enabled = true
308
+ config.reminder_days = [1, 3, 7, 10, 14, 18]
309
+ config.inactivity_close_days = 30
310
+ config.waiting_on_user_label = 'waiting-on-user'
311
+ config.waiting_on_approval_label = 'waiting-on-approval'
312
+ config.user_inactive_label = 'user-inactive'
313
+ ```
314
+
315
+ ## Auto-archiving
316
+
317
+ | Option | Type | Default | Description |
318
+ |---|---|---|---|
319
+ | `archiving_enabled` | `Boolean` | `true` | Whether the archive sweep performs any work. |
320
+ | `archive_closed_after_days` | `Integer` | `90` | Days after `closed_at` at which a non-inactive-closed issue becomes an archive candidate. |
321
+ | `archived_label` | `String` | `'archived'` | Label added to archived issues; also used by the sweep as a skip marker. |
322
+
323
+ The archive sweep piggybacks `RemindersSweepJob`. Inactivity-closed and non-PMS issues are excluded.
324
+
325
+ ```ruby
326
+ config.archiving_enabled = true
327
+ config.archive_closed_after_days = 90
328
+ config.archived_label = 'archived'
329
+ ```
330
+
331
+ ## AWS webhook
332
+
333
+ | Option | Type | Default | Description |
334
+ |---|---|---|---|
335
+ | `sns_topic_arn` | `String, nil` | `nil` | Expected SNS topic ARN for AWS webhook validation. |
336
+ | `aws_service_identifier` | `String, nil` | `nil` | Suffix matched against ECS event resource ARNs (e.g. `'my-app-production-2-web-server'`). |
337
+ | `production_commit_sha` | `String, nil` | `nil` | Prefix-matched against issue metadata `commit_sha` on `SERVICE_DEPLOYMENT_COMPLETED` events. |
338
+ | `process_aws_webhooks` | `Boolean` | `Rails.env.production?` | Whether to process incoming AWS webhook events. |
339
+ | `sns_verifier_class` | `Class` | `Aws::SNS::MessageVerifier` (when defined) | Class instantiated per request for SNS signature verification. Must respond to `authenticate!(raw_body)`. |
340
+ | `sns_verifier_error` | `Class` | `Aws::SNS::MessageVerifier::VerificationError` (when defined) | Exception class rescued during SNS signature verification. |
341
+
342
+ `production_commit_sha` is prefix-matched against issue metadata `commit_sha` on `SERVICE_DEPLOYMENT_COMPLETED` events.
343
+ `sns_verifier_class` must respond to `authenticate!(raw_body)`.
344
+
345
+ ```ruby
346
+ config.sns_topic_arn = 'arn:aws:sns:us-east-1:123456:ecs-deploy-topic'
347
+ config.aws_service_identifier = 'myapp-production-web'
348
+ config.production_commit_sha = Rails.configuration.x.image_tag
349
+ config.process_aws_webhooks = Rails.env.production?
350
+ config.sns_verifier_class = Aws::SNS::MessageVerifier
351
+ config.sns_verifier_error = Aws::SNS::MessageVerifier::VerificationError
352
+ ```
353
+
354
+ ## Caching
355
+
356
+ | Option | Type | Default | Description |
357
+ |---|---|---|---|
358
+ | `cache_enabled` | `Boolean` | `true` | ETag-based HTTP caching of GitHub reads via `Rails.cache`. |
359
+ | `cache_version` | `String, nil` | `nil` | Opaque string baked into every PMS cache key; bump to invalidate. |
360
+
361
+ ```ruby
362
+ config.cache_enabled = true
363
+ config.cache_version = Rails.configuration.x.image_tag
364
+ ```
365
+
366
+ ## Route mounting
367
+
368
+ | Option | Type | Default | Description |
369
+ |---|---|---|---|
370
+ | `mount_groups` | `Hash{Symbol => Boolean}` | `{ webhooks: true, issues: true, projects: true }` | Per-group route mounting toggles. Set a key to `false` to skip mounting that group. |
371
+
372
+ ```ruby
373
+ config.mount_groups = { webhooks: true, issues: true, projects: true }
374
+ ```
375
+
376
+ ## Boot behavior
377
+
378
+ | Option | Type | Default | Description |
379
+ |---|---|---|---|
380
+ | `eager_load_controllers_on_boot` | `Boolean` | `false` | Eager-load engine controllers in `after_initialize`. |
381
+
382
+ Opt in if the host app probes engine controllers via `defined?` in dev mode. When `true`, the
383
+ engine walks `app/controllers` once on boot so `defined?(PlanMyStuff::SomeController)` resolves
384
+ without first referencing the constant.
385
+
386
+ ```ruby
387
+ config.eager_load_controllers_on_boot = true
388
+ ```
389
+
390
+ ## Controller overrides
391
+
392
+ | Option | Type | Default | Description |
393
+ |---|---|---|---|
394
+ | `controllers` | `Hash{Symbol => String}` | `{}` | Per-route controller overrides. Keys are controllable route symbols; values are fully-qualified controller paths. |
395
+
396
+ Per-route controller overrides. Keys are the controllable route symbols defined in
397
+ `PlanMyStuff::Configuration::DEFAULT_CONTROLLERS`; values are fully-qualified controller paths. Unset keys fall back to
398
+ the gem default.
399
+
400
+ ```ruby
401
+ config.controllers[:issues] = 'my_app/issues'
402
+ ```
403
+
404
+ Controllable keys (with gem defaults):
405
+
406
+ | Key | Default |
407
+ |---|---|
408
+ | `:issues` | `plan_my_stuff/issues` |
409
+ | `:comments` | `plan_my_stuff/comments` |
410
+ | `:labels` | `plan_my_stuff/labels` |
411
+ | `:projects` | `plan_my_stuff/projects` |
412
+ | `:project_items` | `plan_my_stuff/project_items` |
413
+ | `:testing_projects` | `plan_my_stuff/testing_projects` |
414
+ | `:testing_project_items` | `plan_my_stuff/testing_project_items` |
415
+ | `:'issues/closures'` | `plan_my_stuff/issues/closures` |
416
+ | `:'issues/viewers'` | `plan_my_stuff/issues/viewers` |
417
+ | `:'issues/takes'` | `plan_my_stuff/issues/takes` |
418
+ | `:'issues/testings'` | `plan_my_stuff/issues/testings` |
419
+ | `:'issues/waitings'` | `plan_my_stuff/issues/waitings` |
420
+ | `:'issues/links'` | `plan_my_stuff/issues/links` |
421
+ | `:'issues/approvals'` | `plan_my_stuff/issues/approvals` |
422
+ | `:'project_items/statuses'` | `plan_my_stuff/project_items/statuses` |
423
+ | `:'project_items/assignments'` | `plan_my_stuff/project_items/assignments` |
424
+ | `:'testing_project_items/results'` | `plan_my_stuff/testing_project_items/results` |
425
+ | `:'webhooks/github'` | `plan_my_stuff/webhooks/github` |
426
+ | `:'webhooks/aws'` | `plan_my_stuff/webhooks/aws` |
427
+
428
+ ### Customizing per-action behavior
429
+
430
+ Every mounted route resolves its controller through `config.controller_for(key)`. Subclass a gem controller in your
431
+ own app and register it to wedge in `before_action`s, authentication, or response tweaks - no monkey patching:
432
+
433
+ ```ruby
434
+ # app/controllers/my_app/issues_controller.rb
435
+ class MyApp::IssuesController < PMS::IssuesController
436
+ before_action :authenticate_user!
437
+ before_action :authorize_ticket_access
438
+ end
439
+ ```
440
+
441
+ ```ruby
442
+ # config/initializers/plan_my_stuff.rb
443
+ PlanMyStuff.configure do |config|
444
+ config.controllers[:issues] = 'my_app/issues'
445
+ end
446
+ ```
447
+
448
+ For per-action side effects (audit log, metrics, notifications) without rewriting the action, yielding actions
449
+ pass their primary object to an optional block on the happy path. Call `super do |obj| ... end` from a subclass:
450
+
451
+ ```ruby
452
+ class MyApp::IssuesController < PMS::IssuesController
453
+ def create
454
+ super do |issue|
455
+ AuditLog.record(actor: current_user, action: :issue_created, target: issue)
456
+ end
457
+ end
458
+
459
+ def update
460
+ super do |issue|
461
+ AuditLog.record(actor: current_user, action: :issue_updated, target: issue)
462
+ end
463
+ end
464
+ end
465
+ ```
466
+
467
+ Contract:
468
+
469
+ - The yield fires on the happy path, after the model load / write succeeds and before the gem's default
470
+ `flash[:success]` + `redirect_to`. Error branches, `not_found`, and authorization redirects do not yield.
471
+ - Read actions (`index`, `show`, `new`, `edit`) yield before the implicit render; `index` yields the collection,
472
+ the rest yield the single object.
473
+ - If your block calls `render` or `redirect_to`, the gem's default response is skipped (the action checks
474
+ `performed?`), so you can fully replace the response from the block.
475
+
476
+ ### Parent controller
477
+
478
+ | Option | Type | Default | Description |
479
+ |---|---|---|---|
480
+ | `parent_controller` | `String` | `'::ApplicationController'` | Parent of `PlanMyStuff::ApplicationController`. |
481
+
482
+ Set this in `config/initializers/plan_my_stuff.rb`; Rails resolves the superclass once at class definition, so the
483
+ value must be set before the engine's controllers load.
484
+
485
+ ```ruby
486
+ config.parent_controller = 'RawrApplicationController'
487
+ ```