plan_my_stuff 0.7.0 → 0.9.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/CONFIGURATION.md +351 -0
  4. data/README.md +100 -103
  5. data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
  11. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
  12. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
  13. data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
  14. data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
  15. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
  16. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
  17. data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
  18. data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
  19. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
  20. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
  21. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
  22. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
  23. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
  24. data/app/jobs/plan_my_stuff/application_job.rb +2 -3
  25. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
  26. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  27. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
  28. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  29. data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
  30. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  31. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
  32. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
  33. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
  34. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
  35. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
  36. data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
  37. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  38. data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
  39. data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
  40. data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
  41. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
  42. data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
  43. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
  44. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
  45. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
  46. data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
  47. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
  48. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
  50. data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
  51. data/config/routes.rb +2 -2
  52. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +52 -14
  53. data/lib/plan_my_stuff/approval.rb +12 -4
  54. data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
  55. data/lib/plan_my_stuff/base_metadata.rb +4 -15
  56. data/lib/plan_my_stuff/base_project.rb +68 -55
  57. data/lib/plan_my_stuff/base_project_item.rb +62 -57
  58. data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
  59. data/lib/plan_my_stuff/client.rb +136 -48
  60. data/lib/plan_my_stuff/comment.rb +59 -57
  61. data/lib/plan_my_stuff/comment_metadata.rb +1 -1
  62. data/lib/plan_my_stuff/configuration.rb +93 -93
  63. data/lib/plan_my_stuff/errors.rb +10 -10
  64. data/lib/plan_my_stuff/graphql/queries.rb +1 -1
  65. data/lib/plan_my_stuff/issue.rb +471 -333
  66. data/lib/plan_my_stuff/issue_metadata.rb +10 -10
  67. data/lib/plan_my_stuff/label.rb +34 -18
  68. data/lib/plan_my_stuff/link.rb +15 -15
  69. data/lib/plan_my_stuff/markdown.rb +12 -6
  70. data/lib/plan_my_stuff/metadata_parser.rb +3 -1
  71. data/lib/plan_my_stuff/notifications.rb +1 -1
  72. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
  73. data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
  74. data/lib/plan_my_stuff/pipeline.rb +61 -83
  75. data/lib/plan_my_stuff/project.rb +4 -4
  76. data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
  77. data/lib/plan_my_stuff/project_metadata.rb +1 -1
  78. data/lib/plan_my_stuff/reminders/closer.rb +1 -1
  79. data/lib/plan_my_stuff/reminders/fire.rb +3 -3
  80. data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
  81. data/lib/plan_my_stuff/repo.rb +12 -6
  82. data/lib/plan_my_stuff/test_helpers.rb +11 -11
  83. data/lib/plan_my_stuff/testing_project.rb +12 -11
  84. data/lib/plan_my_stuff/testing_project_item.rb +11 -9
  85. data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
  86. data/lib/plan_my_stuff/version.rb +1 -1
  87. data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
  88. data/lib/plan_my_stuff.rb +26 -2
  89. data/lib/tasks/plan_my_stuff.rake +33 -20
  90. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4ce40c49bbe2d0ef870cf9983a5571631bdcce7f3ce52c31d5cf9885b143e3b
4
- data.tar.gz: 8e62775d14932c50fe1b69cf76990345adf697dbdf509603eb7d5b4e35cc982f
3
+ metadata.gz: 6457257ef431e923519943cd227ffc70488b023467253873e20f52c47fd75f52
4
+ data.tar.gz: b5a16da4dc6c6c3ce45b307879f5146720f2b878809b7a071b7cc9e33986d210
5
5
  SHA512:
6
- metadata.gz: cf7fcd9a19a7845293c4b52f69ac7ef0cd9aa966bd0ddeb8851521d842e21f98a6e8ef49996125044bc453d99336ea086852d2d605951d348052a3ce16d2f250
7
- data.tar.gz: 9f30fdb98fd6977eeb4362fe41f99472b4771299fa123a080bc83900c65bb794f69f19d5cdb1642efb4867435413ce01169014555641264896b1465b54dc6952
6
+ metadata.gz: efee5f594d9bd014b0ce575781790768a8d51277010d9ad9902ac11beea06c17d39232d65a77532768326eccece3ebcbe3bdb18c464341c963eda9b12b1d6075
7
+ data.tar.gz: ef94bc70d08874c2347e243d0f7967ca7bca2e631cda775494a714df6501d22fe5011a09db805dbb53f5fe8397eb32b8bdf19bbf8a235da39d3f5dd4810c011a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Breaking
6
+
7
+ - `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
8
+
9
+ ### Added
10
+
11
+ - `CONFIGURATION.md` is now available bundled with the gem
12
+
13
+ ### Changed
14
+
15
+ - `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
16
+ - `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
17
+
18
+ ## 0.8.0
19
+
20
+ ### Breaking
21
+
22
+ - 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.
23
+ - 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
24
+ - `Issue#add_viewers` / `Issue#remove_viewers` renamed to `Issue#add_viewers!` / `Issue#remove_viewers!` for consistency with other mutating instance methods (`approve!`, `close!`, etc.)
25
+ - `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`)
26
+ - `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"
27
+ - `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"
28
+
29
+ ### Added
30
+
31
+ - `CONFIGURATION.md` documents every `PlanMyStuff::Configuration` option grouped by concern; `README.md` Configuration section trimmed to the two required options plus a link
32
+ - 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`
33
+ - 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`
34
+ - 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
35
+ - `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`
36
+ - `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
37
+ - `Issue.check_import!(import_id, repo:)` polls a previously-submitted import for its status
38
+ - `Issue#created_at` / `Comment#created_at` attributes hydrated from GitHub's `created_at` field
39
+ - `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
40
+ - `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
41
+ - `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
42
+ - `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)`
43
+ - `Issue#approve!` now accepts either `pending` or `rejected` source state; raises `ValidationError` only on `approved -> approved`. Clears `rejected_at` on the rejected -> approved transition
44
+ - `Issue#rejected_approvals` reader returns the subset of approvers with `status == 'rejected'`
45
+ - 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)
46
+ - 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
47
+
3
48
  ## 0.7.0
4
49
 
5
50
  ### Added
@@ -17,7 +62,7 @@
17
62
  - `Pipeline.submit!` removed; consuming apps that called it directly should switch to `ProjectItem.create!` + `Pipeline.take!`
18
63
  - `Pipeline::Status::SUBMITTED` and `Pipeline::Status::TESTING` constants removed; `Status::ALL` no longer includes them
19
64
  - `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
20
- - Active pipeline status set is now `Started In Review Ready for Release Release in Progress Completed`
65
+ - Active pipeline status set is now `Started -> In Review -> Ready for Release -> Release in Progress -> Completed`
21
66
 
22
67
  ### Added
23
68
 
data/CONFIGURATION.md ADDED
@@ -0,0 +1,351 @@
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
+
41
+ ```ruby
42
+ config.repos = { element: 'YourOrg/Element', underwriter: 'YourOrg/Underwriter' }
43
+ config.default_repo = :element
44
+ ```
45
+
46
+ `repos` can be mutated via `config.repos[:key] = '...'` or set via `config.repos = { key: 'MyOrg/MyRepo' }`
47
+
48
+ ## Projects
49
+
50
+ | Option | Type | Default | Description |
51
+ |---|---|---|---|
52
+ | `default_project_number` | `Integer, nil` | `nil` | Default Projects V2 number for `add_to_project: true`. |
53
+ | `testing_template_project_number` | `Integer, nil` | `nil` | Project to clone in `TestingProject.create!` instead of bootstrapping fields. |
54
+
55
+ ```ruby
56
+ config.default_project_number = 14
57
+ config.testing_template_project_number = 42
58
+ ```
59
+
60
+ ## App identity
61
+
62
+ | Option | Type | Default | Description |
63
+ |---|---|---|---|
64
+ | `app_name` | `String, nil` | `nil` | Stored in metadata so subscribers can attribute writes. |
65
+ | `issues_url_prefix` | `String, nil` | `nil` | Prefix for `Issue#user_link`; the gem appends the issue number. |
66
+
67
+ ```ruby
68
+ config.app_name = 'MyApp'
69
+
70
+ url_options = Rails.application.config.action_mailer.default_url_options
71
+ config.issues_url_prefix = "#{url_options[:protocol] || 'http'}://#{url_options[:host]}/issues"
72
+ ```
73
+
74
+ ## User integration
75
+
76
+ | Option | Type | Default | Description |
77
+ |---|---|---|---|
78
+ | `user_class` | `String` | `'User'` | Consuming app's user model class name, constantized for lookups. |
79
+ | `display_name_method` | `Symbol` | `:to_s` | Method called on a user to get the display name for comment headers. |
80
+ | `user_id_method` | `Symbol` | `:id` | Method called on a user to extract the app-side user ID. |
81
+ | `support_method` | `Symbol, Proc` | `:support?` | Method name on the user, or a proc receiving the user, returning whether they're support staff. |
82
+ | `github_login_for` | `Hash{Object => String}` | `{}` | Maps app user id (from `user_id_method`) to GitHub login. Powers the Take UI. |
83
+
84
+ `support_method` may be a method name on the user object or a proc that receives the user and returns boolean.
85
+ `github_login_for` maps app user id (whatever `user_id_method` returns) to GitHub login and powers the Take UI.
86
+
87
+ ```ruby
88
+ config.user_class = 'User'
89
+ config.display_name_method = :full_name
90
+ config.user_id_method = :id
91
+ config.support_method = :support?
92
+ # or: config.support_method = -> (user) { user.role.in?(%w[support admin]) }
93
+ config.github_login_for = {
94
+ 1 => 'some_username',
95
+ 2 => 'octocat',
96
+ }
97
+ ```
98
+
99
+ ## Engine authentication
100
+
101
+ | Option | Type | Default | Description |
102
+ |---|---|---|---|
103
+ | `authenticate_with` | `Proc` | `nil` | Block executed as a `before_action` on every engine controller. |
104
+
105
+ ```ruby
106
+ config.authenticate_with do
107
+ redirect_to main_app.login_path unless current_user
108
+ end
109
+ ```
110
+
111
+ ## Markdown rendering
112
+
113
+ | Option | Type | Default | Description |
114
+ |---|---|---|---|
115
+ | `markdown_renderer` | `Symbol` | `:commonmarker` | Which markdown gem to use: `:commonmarker`, `:redcarpet`, or `nil` (raw HTML-escaped). |
116
+ | `markdown_options` | `Hash` | `{}` | Default options passed to the renderer. Per-call options merge on top of these. |
117
+
118
+ Set `markdown_renderer` to `:commonmarker`, `:redcarpet`, or `nil` (raw HTML-escaped). The chosen gem must be in your
119
+ Gemfile.
120
+
121
+ ```ruby
122
+ config.markdown_renderer = :commonmarker
123
+ config.markdown_options = { render: { hardbreaks: true } }
124
+ ```
125
+
126
+ ## Notification actor
127
+
128
+ | Option | Type | Default | Description |
129
+ |---|---|---|---|
130
+ | `current_user` | `Proc, nil` | `nil` | Fallback actor for notification events when `user:` is not passed. |
131
+
132
+ ```ruby
133
+ config.current_user = -> { Current.user }
134
+ ```
135
+
136
+ ## Controller rescue
137
+
138
+ | Option | Type | Default | Description |
139
+ |---|---|---|---|
140
+ | `controller_rescue` | `Proc, nil` | `nil` | Receives the rescued exception. Forward to your monitoring service. |
141
+
142
+ ```ruby
143
+ config.controller_rescue = -> (error) { MonitoringService.notice_error(error) }
144
+ ```
145
+
146
+ ## Custom fields
147
+
148
+ App-defined fields stored in metadata. Keys are field names; values are hashes with `:type` and `:required`.
149
+ Supported types: `:string`, `:integer`, `:boolean`, `:array`, `:hash`.
150
+
151
+ | Option | Type | Default | Description |
152
+ |---|---|---|---|
153
+ | `custom_fields` | `Hash{Symbol => Hash}` | `{}` | Shared field definitions across all contexts. |
154
+ | `issue_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Issue-only definitions; merged on top of shared. |
155
+ | `comment_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Comment-only definitions; merged on top of shared. |
156
+ | `project_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Project-only definitions; merged on top of shared. |
157
+ | `testing_custom_fields` | `Hash{Symbol => Hash}` | `{}` | Testing-project-only definitions; merged on top of shared. |
158
+
159
+ Context-specific config wins on key conflicts.
160
+
161
+ ```ruby
162
+ config.custom_fields = {
163
+ notification_recipients: { type: :array, required: true },
164
+ }
165
+ config.issue_custom_fields = {
166
+ ticket_type: { type: :string, required: true },
167
+ }
168
+ config.comment_custom_fields = {
169
+ internal_note: { type: :boolean },
170
+ }
171
+ config.project_custom_fields = {
172
+ team: { type: :string },
173
+ }
174
+ config.testing_custom_fields = {
175
+ test_plan_url: { type: :string },
176
+ }
177
+ ```
178
+
179
+ ## Issue types
180
+
181
+ | Option | Type | Default | Description |
182
+ |---|---|---|---|
183
+ | `issue_types` | `Hash{String => String}` | `{}` | Maps the gem's canonical issue type names to your org's display names. |
184
+
185
+ Maps the gem's canonical type names (`'Bug'`, `'Feature'`, `'IT Issue / Hardware'`, `'Other'`, `'Performance'`,
186
+ `'Question'`, `'Task'`) to whatever your org uses. Missing keys pass through unchanged.
187
+
188
+ ```ruby
189
+ config.issue_types = {
190
+ 'Bug' => 'User Bug',
191
+ 'Feature' => 'Enhancement',
192
+ }
193
+ ```
194
+
195
+ ## Release pipeline
196
+
197
+ | Option | Type | Default | Description |
198
+ |---|---|---|---|
199
+ | `pipeline_enabled` | `Boolean` | `true` | Whether the release pipeline feature is enabled. |
200
+ | `pipeline_project_number` | `Integer, nil` | `nil` | Projects V2 number for the pipeline board. Falls back to `default_project_number`. |
201
+ | `webhook_secret` | `String, nil` | `nil` | HMAC secret for GitHub webhook signature verification. Required when webhook routes are mounted. |
202
+ | `pipeline_statuses` | `Hash{String => String}` | `{}` | Display aliases for canonical pipeline status names. |
203
+ | `pipeline_testing_field_name` | `String` | `'Testing'` | Display name for the pipeline project's `Testing` single-select field. |
204
+ | `pipeline_testing_values` | `Hash{Symbol => String}` | `{ active: 'Testing', inactive: 'Not testing' }` | Display labels for the canonical `:active`/`:inactive` testing options. |
205
+ | `pipeline_completion_purge_enabled` | `Boolean` | `true` | Whether the sweep removes aged-out `Completed` items from the pipeline. |
206
+ | `pipeline_completion_ttl_hours` | `Integer` | `24` | Hours after a `Completed` item's last update at which the sweep removes it. |
207
+ | `main_branch` | `String` | `'main'` | Branch PRs merge into for the "Ready for Release" transition. |
208
+ | `production_branch` | `String` | `'production'` | Branch PRs mereg into for the "Release in Progress" transition. |
209
+
210
+ `pipeline_statuses` aliases the canonical status names (`'Submitted'`, `'Started'`, `'In Review'`, `'Testing'`,
211
+ `'Ready for Release'`, `'Release in Progress'`, `'Completed'`) for display only. The constants remain the internal
212
+ identifiers.
213
+
214
+ `pipeline_completion_*` controls the sweep that removes aged-out `Completed` items from the pipeline. `webhook_secret`
215
+ is required when webhook routes are mounted.
216
+
217
+ ```ruby
218
+ config.pipeline_enabled = true
219
+ config.pipeline_project_number = 14
220
+ config.webhook_secret = Rails.application.credentials.dig(:plan_my_stuff, :webhook_secret)
221
+ config.pipeline_statuses = {
222
+ 'Submitted' => 'Triaged',
223
+ 'Completed' => 'Done',
224
+ }
225
+ config.pipeline_testing_field_name = 'Testing'
226
+ config.pipeline_testing_values = { active: 'Testing', inactive: 'Not testing' }
227
+ config.pipeline_completion_purge_enabled = true
228
+ config.pipeline_completion_ttl_hours = 24
229
+ config.main_branch = 'main'
230
+ config.production_branch = 'production'
231
+ ```
232
+
233
+ ## Follow-up reminders
234
+
235
+ | Option | Type | Default | Description |
236
+ |---|---|---|---|
237
+ | `reminders_enabled` | `Boolean` | `true` | Whether the reminders sweep performs any work. |
238
+ | `reminder_days` | `Array<Integer>` | `[1, 3, 7, 10, 14, 18]` | Days-since-waiting at which reminder events fire. |
239
+ | `inactivity_close_days` | `Integer` | `30` | Days of inactivity after which the sweep auto-closes a waiting issue. |
240
+ | `waiting_on_user_label` | `String` | `'waiting-on-user'` | Label flagging issues waiting on an end-user reply. |
241
+ | `waiting_on_approval_label` | `String` | `'waiting-on-approval'` | Label flagging issues waiting on pending approvals. |
242
+ | `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. |
243
+
244
+ Per-issue reminder override: `issue.metadata.reminder_days = [...]`.
245
+
246
+ ```ruby
247
+ config.reminders_enabled = true
248
+ config.reminder_days = [1, 3, 7, 10, 14, 18]
249
+ config.inactivity_close_days = 30
250
+ config.waiting_on_user_label = 'waiting-on-user'
251
+ config.waiting_on_approval_label = 'waiting-on-approval'
252
+ config.user_inactive_label = 'user-inactive'
253
+ ```
254
+
255
+ ## Auto-archiving
256
+
257
+ | Option | Type | Default | Description |
258
+ |---|---|---|---|
259
+ | `archiving_enabled` | `Boolean` | `true` | Whether the archive sweep performs any work. |
260
+ | `archive_closed_after_days` | `Integer` | `90` | Days after `closed_at` at which a non-inactive-closed issue becomes an archive candidate. |
261
+ | `archived_label` | `String` | `'archived'` | Label added to archived issues; also used by the sweep as a skip marker. |
262
+
263
+ The archive sweep piggybacks `RemindersSweepJob`. Inactivity-closed and non-PMS issues are excluded.
264
+
265
+ ```ruby
266
+ config.archiving_enabled = true
267
+ config.archive_closed_after_days = 90
268
+ config.archived_label = 'archived'
269
+ ```
270
+
271
+ ## AWS webhook
272
+
273
+ | Option | Type | Default | Description |
274
+ |---|---|---|---|
275
+ | `sns_topic_arn` | `String, nil` | `nil` | Expected SNS topic ARN for AWS webhook validation. |
276
+ | `aws_service_identifier` | `String, nil` | `nil` | Suffix matched against ECS event resource ARNs (e.g. `'my-app-production-2-web-server'`). |
277
+ | `production_commit_sha` | `String, nil` | `nil` | Prefix-matched against issue metadata `commit_sha` on `SERVICE_DEPLOYMENT_COMPLETED` events. |
278
+ | `process_aws_webhooks` | `Boolean` | `Rails.env.production?` | Whether to process incoming AWS webhook events. |
279
+ | `sns_verifier_class` | `Class` | `Aws::SNS::MessageVerifier` (when defined) | Class instantiated per request for SNS signature verification. Must respond to `authenticate!(raw_body)`. |
280
+ | `sns_verifier_error` | `Class` | `Aws::SNS::MessageVerifier::VerificationError` (when defined) | Exception class rescued during SNS signature verification. |
281
+
282
+ `production_commit_sha` is prefix-matched against issue metadata `commit_sha` on `SERVICE_DEPLOYMENT_COMPLETED` events.
283
+ `sns_verifier_class` must respond to `authenticate!(raw_body)`.
284
+
285
+ ```ruby
286
+ config.sns_topic_arn = 'arn:aws:sns:us-east-1:123456:ecs-deploy-topic'
287
+ config.aws_service_identifier = 'myapp-production-web'
288
+ config.production_commit_sha = Rails.configuration.x.image_tag
289
+ config.process_aws_webhooks = Rails.env.production?
290
+ config.sns_verifier_class = Aws::SNS::MessageVerifier
291
+ config.sns_verifier_error = Aws::SNS::MessageVerifier::VerificationError
292
+ ```
293
+
294
+ ## Caching
295
+
296
+ | Option | Type | Default | Description |
297
+ |---|---|---|---|
298
+ | `cache_enabled` | `Boolean` | `true` | ETag-based HTTP caching of GitHub reads via `Rails.cache`. |
299
+ | `cache_version` | `String, nil` | `nil` | Opaque string baked into every PMS cache key; bump to invalidate. |
300
+
301
+ ```ruby
302
+ config.cache_enabled = true
303
+ config.cache_version = Rails.configuration.x.image_tag
304
+ ```
305
+
306
+ ## Route mounting
307
+
308
+ | Option | Type | Default | Description |
309
+ |---|---|---|---|
310
+ | `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. |
311
+
312
+ ```ruby
313
+ config.mount_groups = { webhooks: true, issues: true, projects: true }
314
+ ```
315
+
316
+ ## Controller overrides
317
+
318
+ | Option | Type | Default | Description |
319
+ |---|---|---|---|
320
+ | `controllers` | `Hash{Symbol => String}` | `{}` | Per-route controller overrides. Keys are controllable route symbols; values are fully-qualified controller paths. |
321
+
322
+ Per-route controller overrides. Keys are the controllable route symbols defined in
323
+ `PlanMyStuff::Configuration::DEFAULT_CONTROLLERS`; values are fully-qualified controller paths. Unset keys fall back to
324
+ the gem default.
325
+
326
+ ```ruby
327
+ config.controllers[:issues] = 'my_app/issues'
328
+ ```
329
+
330
+ Controllable keys (with gem defaults):
331
+
332
+ | Key | Default |
333
+ |---|---|
334
+ | `:issues` | `plan_my_stuff/issues` |
335
+ | `:comments` | `plan_my_stuff/comments` |
336
+ | `:labels` | `plan_my_stuff/labels` |
337
+ | `:projects` | `plan_my_stuff/projects` |
338
+ | `:project_items` | `plan_my_stuff/project_items` |
339
+ | `:testing_projects` | `plan_my_stuff/testing_projects` |
340
+ | `:testing_project_items` | `plan_my_stuff/testing_project_items` |
341
+ | `:'issues/closures'` | `plan_my_stuff/issues/closures` |
342
+ | `:'issues/viewers'` | `plan_my_stuff/issues/viewers` |
343
+ | `:'issues/takes'` | `plan_my_stuff/issues/takes` |
344
+ | `:'issues/waitings'` | `plan_my_stuff/issues/waitings` |
345
+ | `:'issues/links'` | `plan_my_stuff/issues/links` |
346
+ | `:'issues/approvals'` | `plan_my_stuff/issues/approvals` |
347
+ | `:'project_items/statuses'` | `plan_my_stuff/project_items/statuses` |
348
+ | `:'project_items/assignments'` | `plan_my_stuff/project_items/assignments` |
349
+ | `:'testing_project_items/results'` | `plan_my_stuff/testing_project_items/results` |
350
+ | `:'webhooks/github'` | `plan_my_stuff/webhooks/github` |
351
+ | `:'webhooks/aws'` | `plan_my_stuff/webhooks/aws` |