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
@@ -2,9 +2,43 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  class Configuration
5
+ # Default controller for each controllable route group. Consuming apps override by assigning values into
6
+ # +controllers+; lookups go through +controller_for+ which falls back to this table.
7
+ #
8
+ # @return [Hash{Symbol => String}]
9
+ #
10
+ DEFAULT_CONTROLLERS = {
11
+ 'issues': 'plan_my_stuff/issues',
12
+ 'comments': 'plan_my_stuff/comments',
13
+ 'labels': 'plan_my_stuff/labels',
14
+ 'projects': 'plan_my_stuff/projects',
15
+ 'project_items': 'plan_my_stuff/project_items',
16
+ 'testing_projects': 'plan_my_stuff/testing_projects',
17
+ 'testing_project_items': 'plan_my_stuff/testing_project_items',
18
+ 'issues/closures': 'plan_my_stuff/issues/closures',
19
+ 'issues/viewers': 'plan_my_stuff/issues/viewers',
20
+ 'issues/takes': 'plan_my_stuff/issues/takes',
21
+ 'issues/testings': 'plan_my_stuff/issues/testings',
22
+ 'issues/waitings': 'plan_my_stuff/issues/waitings',
23
+ 'issues/links': 'plan_my_stuff/issues/links',
24
+ 'issues/approvals': 'plan_my_stuff/issues/approvals',
25
+ 'project_items/statuses': 'plan_my_stuff/project_items/statuses',
26
+ 'project_items/assignments': 'plan_my_stuff/project_items/assignments',
27
+ 'testing_project_items/results': 'plan_my_stuff/testing_project_items/results',
28
+ 'webhooks/github': 'plan_my_stuff/webhooks/github',
29
+ 'webhooks/aws': 'plan_my_stuff/webhooks/aws',
30
+ }.freeze
31
+
5
32
  # @return [String] GitHub PAT with repo and project scopes. Required.
6
33
  attr_accessor :access_token
7
34
 
35
+ # Classic GitHub PAT used for the Issues Import API (golden-comet-preview). Requires the +repo+ scope (admin-level
36
+ # repository access). Fine-grained tokens are not supported by that endpoint.
37
+ #
38
+ # @return [String, nil]
39
+ #
40
+ attr_accessor :import_access_token
41
+
8
42
  # @return [String] GitHub organization name. Required.
9
43
  attr_accessor :organization
10
44
 
@@ -14,6 +48,11 @@ module PlanMyStuff
14
48
  # @return [Integer, nil] default GitHub Projects V2 number for add_to_project calls
15
49
  attr_accessor :default_project_number
16
50
 
51
+ # @return [Integer, nil] GitHub Projects V2 number of the template project to clone when creating new
52
+ # TestingProjects. When set, TestingProject.create! copies the template (preserving its fields and board layout)
53
+ # instead of bootstrapping fields from scratch. Leave nil to use the default bootstrap-fields path.
54
+ attr_accessor :testing_template_project_number
55
+
17
56
  # @return [String] consuming app's user model class name, constantized for lookups
18
57
  attr_accessor :user_class
19
58
 
@@ -23,8 +62,8 @@ module PlanMyStuff
23
62
  # @return [Symbol] method called on user object to extract the app-side user ID
24
63
  attr_accessor :user_id_method
25
64
 
26
- # Determines if a user is support staff. Symbol (method name on user) or Proc that
27
- # receives the user object and returns boolean.
65
+ # Determines if a user is support staff. Symbol (method name on user) or Proc that receives the user object and
66
+ # returns boolean.
28
67
  #
29
68
  # @return [Symbol, Proc]
30
69
  #
@@ -33,8 +72,7 @@ module PlanMyStuff
33
72
  # @return [Symbol] which markdown gem to use: :commonmarker or :redcarpet
34
73
  attr_accessor :markdown_renderer
35
74
 
36
- # Default options passed to the markdown renderer. Per-call options in
37
- # Markdown.render merge on top of these.
75
+ # Default options passed to the markdown renderer. Per-call options in Markdown.render merge on top of these.
38
76
  #
39
77
  # For :commonmarker - passed as `options:` to `Commonmarker.to_html`
40
78
  # e.g. `{ render: { hardbreaks: true } }`
@@ -47,35 +85,73 @@ module PlanMyStuff
47
85
  #
48
86
  attr_accessor :markdown_options
49
87
 
50
- # Proc returning boolean, or nil (always send). When it returns false the request is
51
- # deferred to a background job instead of hitting GitHub.
88
+ # Fallback actor for notification events when a caller does not pass +user:+. Set to a proc/lambda that returns the
89
+ # current request user.
90
+ #
91
+ # Example: +config.current_user = -> { Current.user }+
52
92
  #
53
93
  # @return [Proc, nil]
54
94
  #
55
- attr_accessor :should_send_request
95
+ attr_accessor :current_user
56
96
 
57
- # Map of action type to job class name for deferred requests.
58
- # Keys: :create_ticket, :post_comment, :update_status.
97
+ # Callback invoked from gem controller +rescue+ blocks just after the error is logged and just before the
98
+ # user-facing redirect/render. Lets consuming apps forward swallowed errors to their monitoring service. Receives
99
+ # the rescued exception.
59
100
  #
60
- # @return [Hash{Symbol => String}]
101
+ # Example:
102
+ # config.controller_rescue = ->(error) { MonitoringService.notice_error(error) }
61
103
  #
62
- attr_accessor :job_classes
104
+ # @return [Proc, nil]
105
+ #
106
+ attr_accessor :controller_rescue
63
107
 
64
- # @return [Proc, nil] custom notifier for deferred requests, or nil to use DeferredMailer
65
- attr_accessor :deferred_notifier
108
+ # Shared field definitions stored in issue/comment metadata. Keys are field names, values are hashes with :type and
109
+ # :required. These fields apply to both issues and comments.
110
+ #
111
+ # @return [Hash{Symbol => Hash}]
112
+ #
113
+ attr_accessor :custom_fields
66
114
 
67
- # @return [String, nil] sender address for built-in deferred request notifications
68
- attr_accessor :deferred_email_from
115
+ # Issue-only field definitions, deep-merged on top of shared custom_fields. Context-specific config wins on key
116
+ # conflicts.
117
+ #
118
+ # @return [Hash{Symbol => Hash}]
119
+ #
120
+ attr_accessor :issue_custom_fields
69
121
 
70
- # @return [String, nil] recipient address for built-in deferred request notifications
71
- attr_accessor :deferred_email_to
122
+ # Comment-only field definitions, deep-merged on top of shared custom_fields. Context-specific config wins on key
123
+ # conflicts.
124
+ #
125
+ # @return [Hash{Symbol => Hash}]
126
+ #
127
+ attr_accessor :comment_custom_fields
72
128
 
73
- # App-defined field definitions stored in issue/comment metadata.
74
- # Keys are field names, values are hashes with :type and :required.
129
+ # Project-only field definitions, deep-merged on top of shared custom_fields. Context-specific config wins on key
130
+ # conflicts.
75
131
  #
76
132
  # @return [Hash{Symbol => Hash}]
77
133
  #
78
- attr_accessor :custom_fields
134
+ attr_accessor :project_custom_fields
135
+
136
+ # Testing-project-only field definitions, deep-merged on top of shared custom_fields. Context-specific config wins
137
+ # on key conflicts.
138
+ #
139
+ # @return [Hash{Symbol => Hash}]
140
+ #
141
+ attr_accessor :testing_custom_fields
142
+
143
+ # Canonical-name to org-side display-name map for GitHub native issue types. Lets the consuming app rename the
144
+ # seven canonical types the gem knows about (+"Bug"+, +"Feature"+, +"IT Issue / Hardware"+, +"Other"+,
145
+ # +"Performance"+, +"Question"+, +"Task"+) to whatever their org actually uses. Both Symbol shortcuts (resolved via
146
+ # the gem's nickname map) and String inputs to +Issue.create!+ / +Issue.update!+ are passed through this map;
147
+ # missing keys fall through unchanged.
148
+ #
149
+ # Example: +config.issue_types = { 'Bug' => 'User Bug', 'Feature' => 'Enhancement' }+ then +issue_type: :feature+
150
+ # or +issue_type: 'Feature'+ both write +'Enhancement'+ to GitHub.
151
+ #
152
+ # @return [Hash{String => String}]
153
+ #
154
+ attr_accessor :issue_types
79
155
 
80
156
  # @return [String, nil] URL prefix for building user-facing ticket URLs in the consuming app
81
157
  attr_accessor :issues_url_prefix
@@ -83,23 +159,296 @@ module PlanMyStuff
83
159
  # @return [String, nil] name of the consuming app, stored in metadata (e.g. "Atlas")
84
160
  attr_accessor :app_name
85
161
 
86
- # Named repo configs. Set via config.repos[:element] = 'BrandsInsurance/Element'.
162
+ # @return [Boolean] whether the release pipeline feature is enabled
163
+ attr_accessor :pipeline_enabled
164
+
165
+ # @return [Integer, nil] GitHub Projects V2 number for the pipeline board (falls back to default_project_number)
166
+ attr_accessor :pipeline_project_number
167
+
168
+ # @return [String, nil] HMAC secret for GitHub webhook signature verification (required when webhooks mounted)
169
+ attr_accessor :webhook_secret
170
+
171
+ # @return [String, nil] expected SNS topic ARN for AWS webhook validation
172
+ attr_accessor :sns_topic_arn
173
+
174
+ # @return [String, nil] suffix matched against ECS event resource ARNs (e.g. 'rawr-production-2-web')
175
+ attr_accessor :aws_service_identifier
176
+
177
+ # @return [String, nil] commit hash of the deploying build, prefix-matched against issue metadata commit_sha
178
+ attr_accessor :production_commit_sha
179
+
180
+ # Canonical status name to display alias map. Allows consuming apps to rename pipeline statuses (e.g. "Started" to
181
+ # "In Progress").
182
+ #
183
+ # @return [Hash{String => String}]
184
+ #
185
+ attr_accessor :pipeline_statuses
186
+
187
+ # Display name for the +Testing+ single-select custom field on the pipeline project. Defaults to +"Testing"+.
188
+ #
189
+ # @return [String]
190
+ #
191
+ attr_accessor :pipeline_testing_field_name
192
+
193
+ # Map of canonical testing field option keys (+:active+, +:inactive+) to display labels. Allows consuming apps to
194
+ # rename the option labels without changing the canonical identifiers.
87
195
  #
88
196
  # @return [Hash{Symbol => String}]
89
197
  #
90
- attr_reader :repos
198
+ attr_accessor :pipeline_testing_values
199
+
200
+ # Canonical Issue Field name to consumer field name map. Lets a consuming org rename the native issue fields the
201
+ # gem refers to internally (e.g. +"Issue Status"+ -> +"Status"+) without touching gem code. Applied via
202
+ # +PlanMyStuff::IssueFieldTranslation+ on reads, writes, and filters; unmapped names pass through unchanged.
203
+ #
204
+ # @return [Hash{String => String}]
205
+ #
206
+ attr_accessor :issue_field_names
207
+
208
+ # Canonical Issue Field value translations, nested by canonical field name:
209
+ # +{ 'Issue Status' => { 'Waiting on Reply' => 'Awaiting Customer', ... } }+. Lets a consuming org rename
210
+ # single-select option labels without touching gem code. Applied via +PlanMyStuff::IssueFieldTranslation+;
211
+ # unmapped fields / values pass through unchanged.
212
+ #
213
+ # @return [Hash{String => Hash{String => String}}]
214
+ #
215
+ attr_accessor :issue_field_values
216
+
217
+ # @return [String] branch name that PRs merge into for "Ready for release" transition
218
+ attr_accessor :main_branch
219
+
220
+ # @return [String] branch name that triggers deployment when a PR merges
221
+ attr_accessor :production_branch
222
+
223
+ # Hash mapping consuming-app user id to GitHub login. Used by the "Take" UI flow to assign the GitHub user when a
224
+ # support user claims an issue. Keys are whatever +config.user_id_method+ returns on the current user.
225
+ #
226
+ # Example: +config.github_login_for = { 1 => 'some_username', 2 => 'octocat' }+
227
+ #
228
+ # @return [Hash{Object => String}]
229
+ #
230
+ attr_accessor :github_login_for
231
+
232
+ # Per-group route mounting toggles. Keys: :webhooks, :issues, :projects. Set a key to false to skip mounting that
233
+ # route group.
234
+ #
235
+ # @return [Hash{Symbol => Boolean}]
236
+ #
237
+ attr_accessor :mount_groups
238
+
239
+ # Per-route controller overrides. Keys are the controllable route symbols defined in +DEFAULT_CONTROLLERS+; values
240
+ # are fully-qualified controller paths (e.g. +'my_app/issues'+). Unset keys fall back to the gem default. Consuming
241
+ # apps typically subclass the gem controller to add before_actions or tweak responses, then swap their subclass in
242
+ # here.
243
+ #
244
+ # @return [Hash{Symbol => String}]
245
+ #
246
+ attr_accessor :controllers
247
+
248
+ # Parent class string for +PlanMyStuff::ApplicationController+. Defaults to +'::ApplicationController'+. Override
249
+ # when the consuming app does not use the +ApplicationController+ name (e.g. +'ActionController::Base'+,
250
+ # +'RawrApplicationController'+). Must be set before the gem's controllers load - the standard
251
+ # +config/initializers/plan_my_stuff.rb+ location is correct, since Rails resolves the superclass once at class
252
+ # definition time.
253
+ #
254
+ # @return [String]
255
+ #
256
+ attr_accessor :parent_controller
257
+
258
+ # Whether to use Rails.cache for ETag-based HTTP caching of GitHub reads. Defaults to true; set to false to bypass
259
+ # the cache entirely.
260
+ #
261
+ # @return [Boolean]
262
+ #
263
+ attr_accessor :cache_enabled
264
+
265
+ # Opaque app-supplied version string embedded in every PMS cache key. Bumping this string invalidates all cached
266
+ # entries from the consuming app's side (e.g. after a deploy or schema change). Defaults to nil.
267
+ #
268
+ # @return [String, nil]
269
+ #
270
+ attr_accessor :cache_version
271
+
272
+ # Whether the reminders sweep performs any work. Defaults to +true+. Set to +false+ in apps that don't want
273
+ # follow-up reminders or inactivity auto-close.
274
+ #
275
+ # @return [Boolean]
276
+ #
277
+ attr_accessor :reminders_enabled
278
+
279
+ # Days-since-waiting at which reminder events fire. Per-issue override via +metadata.reminder_days+.
280
+ #
281
+ # @return [Array<Integer>]
282
+ #
283
+ attr_accessor :reminder_days
284
+
285
+ # Days of inactivity after which the sweep auto-closes a waiting issue.
286
+ #
287
+ # @return [Integer]
288
+ #
289
+ attr_accessor :inactivity_close_days
290
+
291
+ # Label name used to flag issues waiting on an end-user reply.
292
+ #
293
+ # @return [String]
294
+ #
295
+ attr_accessor :waiting_on_user_label
296
+
297
+ # Label name used to flag issues waiting on pending approvals.
298
+ #
299
+ # @return [String]
300
+ #
301
+ attr_accessor :waiting_on_approval_label
302
+
303
+ # Label name applied to issues auto-closed by the inactivity sweep. Removed when an issue is auto-reopened via a
304
+ # user reply.
305
+ #
306
+ # @return [String]
307
+ #
308
+ attr_accessor :user_inactive_label
309
+
310
+ # Whether the archive sweep performs any work. Defaults to +true+. Set to +false+ in apps that don't want
311
+ # auto-archiving of aged-closed issues.
312
+ #
313
+ # @return [Boolean]
314
+ #
315
+ attr_accessor :archiving_enabled
316
+
317
+ # Days after +closed_at+ at which a non-inactive-closed issue becomes an archive candidate.
318
+ #
319
+ # @return [Integer]
320
+ #
321
+ attr_accessor :archive_closed_after_days
322
+
323
+ # Label name added to archived issues. Also used by the sweep as a skip marker to avoid re-archiving the same
324
+ # issue.
325
+ #
326
+ # @return [String]
327
+ #
328
+ attr_accessor :archived_label
329
+
330
+ # Whether the pipeline sweep removes aged-out +Completed+ items. Defaults to +true+. Set to +false+ to keep items
331
+ # in +Completed+ indefinitely.
332
+ #
333
+ # @return [Boolean]
334
+ #
335
+ attr_accessor :pipeline_completion_purge_enabled
336
+
337
+ # Hours after a project item's last update at which the sweep removes it from the pipeline if its status is
338
+ # +Completed+. Defaults to +24+.
339
+ #
340
+ # @return [Integer]
341
+ #
342
+ attr_accessor :pipeline_completion_ttl_hours
343
+
344
+ # Whether to process incoming AWS webhook events. Defaults to +Rails.env.production?+.
345
+ #
346
+ # @return [Boolean]
347
+ #
348
+ attr_accessor :process_aws_webhooks
349
+
350
+ # Class instantiated per request for SNS signature verification. Must respond to +authenticate!(raw_body)+.
351
+ #
352
+ # @return [Class]
353
+ #
354
+ attr_accessor :sns_verifier_class
355
+
356
+ # Exception class rescued during SNS signature verification.
357
+ #
358
+ # @return [Class]
359
+ #
360
+ attr_accessor :sns_verifier_error
361
+
362
+ # Named repo configs. Set via config.repos[:element] = 'BrandsInsurance/Element', or assign a whole hash with
363
+ # config.repos = { element: 'BrandsInsurance/Element', underwriter: 'BrandsInsurance/Underwriter' }.
364
+ #
365
+ # @return [Hash{Symbol => String}]
366
+ #
367
+ attr_accessor :repos
368
+
369
+ # Human-readable repo names used as the +to_param+ prefix on +PlanMyStuff::Issue+ instances. Symbol-keyed
370
+ # against +repos+ -- missing keys fall back to +key.to_s.titleize+, so only entries that diverge from a simple
371
+ # +titleize+ of the key (e.g. +:safety+ -> +"Compliance"+) need to be listed.
372
+ #
373
+ # @return [Hash{Symbol => String}]
374
+ #
375
+ attr_accessor :repo_nicknames
376
+
377
+ # Bare repo name (under +config.organization+) that stores uploaded attachment binaries. Defaults to
378
+ # +'pms-attachments'+. The repo must exist; the uploader does not create it. Attachments commit onto
379
+ # +config.main_branch+ and live under +<repo_key_or_name>/issue-<number>/<uuid>.<ext>+.
380
+ #
381
+ # @return [String]
382
+ #
383
+ attr_accessor :attachment_repo
384
+
385
+ # Whether GitHub's Issue Fields (public preview) are wired up for the configured org. Defaults to +true+ (opt-out):
386
+ # when +false+, +Issue#issue_fields+ returns an empty +IssueFieldValueSet+ without making a request and
387
+ # +Issue#set_issue_fields!+ / +IssueField.list+ raise +IssueFieldsNotEnabledError+. Set to +false+ if your org has
388
+ # not been admitted to the Issue Fields preview, to avoid raw GraphQL errors from GitHub.
389
+ #
390
+ # @return [Boolean]
391
+ #
392
+ attr_accessor :issue_fields_enabled
393
+
394
+ # Whether to eager-load the engine's controllers on host boot. Defaults to +false+ (opt-in). When +true+, the engine
395
+ # walks +app/controllers+ during +after_initialize+ so +defined?(PlanMyStuff::SomeController)+ resolves without
396
+ # first referencing the constant. Enable in host apps that rely on +defined?+ probes against engine controllers in
397
+ # dev mode.
398
+ #
399
+ # @return [Boolean]
400
+ #
401
+ attr_accessor :eager_load_controllers_on_boot
91
402
 
92
403
  # @return [Configuration]
93
404
  def initialize
94
405
  @repos = {}
406
+ @repo_nicknames = {}
407
+ @attachment_repo = 'pms-attachments'
95
408
  @user_class = 'User'
96
409
  @display_name_method = :to_s
97
410
  @user_id_method = :id
98
411
  @support_method = :support?
99
412
  @markdown_renderer = :commonmarker
100
413
  @markdown_options = {}
101
- @job_classes = {}
102
414
  @custom_fields = {}
415
+ @issue_custom_fields = {}
416
+ @comment_custom_fields = {}
417
+ @project_custom_fields = {}
418
+ @testing_custom_fields = {}
419
+ @issue_types = {}
420
+ @pipeline_enabled = true
421
+ @pipeline_statuses = {}
422
+ @pipeline_testing_field_name = PlanMyStuff::Pipeline::Testing::FIELD_NAME
423
+ @pipeline_testing_values = PlanMyStuff::Pipeline::Testing::VALUES.dup
424
+ @issue_field_names = {}
425
+ @issue_field_values = {}
426
+ @main_branch = 'main'
427
+ @production_branch = 'production'
428
+ @mount_groups = { webhooks: true, issues: true, projects: true }
429
+ @controllers = {}
430
+ @parent_controller = '::ApplicationController'
431
+ @cache_enabled = true
432
+ @github_login_for = {}
433
+ @reminders_enabled = true
434
+ @reminder_days = [1, 3, 7, 10, 14, 18].freeze
435
+ @inactivity_close_days = 30
436
+ @waiting_on_user_label = 'waiting-on-user'
437
+ @waiting_on_approval_label = 'waiting-on-approval'
438
+ @user_inactive_label = 'user-inactive'
439
+ @archiving_enabled = true
440
+ @archive_closed_after_days = 90
441
+ @archived_label = 'archived'
442
+ @pipeline_completion_purge_enabled = true
443
+ @pipeline_completion_ttl_hours = 24
444
+ @issue_fields_enabled = true
445
+ @eager_load_controllers_on_boot = false
446
+ @process_aws_webhooks = Rails.env.production?
447
+ @sns_verifier_class = ::Aws::SNS::MessageVerifier if defined?(::Aws::SNS::MessageVerifier)
448
+ @sns_verifier_error =
449
+ if defined?(::Aws::SNS::MessageVerifier::VerificationError)
450
+ ::Aws::SNS::MessageVerifier::VerificationError
451
+ end
103
452
  end
104
453
 
105
454
  # Sets the authentication block for engine controllers.
@@ -125,13 +474,95 @@ module PlanMyStuff
125
474
  missing << 'access_token' if access_token.nil? || access_token.to_s.strip.empty?
126
475
  missing << 'organization' if organization.nil? || organization.to_s.strip.empty?
127
476
 
128
- return if missing.empty?
477
+ if missing.present?
478
+ raise(
479
+ PlanMyStuff::ConfigurationError,
480
+ "Missing required PlanMyStuff configuration: #{missing.join(', ')}",
481
+ )
482
+ end
483
+
484
+ validate_repo_nicknames!
485
+ end
486
+
487
+ # Returns the merged custom fields schema for the given context. Context-specific fields deep-merge on top of
488
+ # shared fields.
489
+ #
490
+ # @param context [Symbol] :issue or :comment
491
+ #
492
+ # @return [Hash{Symbol => Hash}]
493
+ #
494
+ def custom_fields_for(context)
495
+ context_fields =
496
+ case context
497
+ when :issue then issue_custom_fields
498
+ when :comment then comment_custom_fields
499
+ when :project then project_custom_fields
500
+ when :testing then testing_custom_fields
501
+ else {}
502
+ end
503
+
504
+ custom_fields.deep_merge(context_fields)
505
+ end
129
506
 
130
- raise(
131
- ConfigurationError,
132
- "Missing required PlanMyStuff configuration: #{missing.join(', ')}",
133
- )
507
+ # Returns the controller path for the given route group, preferring a consuming-app override from +controllers+
508
+ # and falling back to the gem default from +DEFAULT_CONTROLLERS+. The returned value always begins with +'/'+ so
509
+ # the isolated engine does not re-prefix it with +plan_my_stuff/+.
510
+ #
511
+ # @raise [KeyError] if +key+ is not a controllable route group
512
+ #
513
+ # @param key [Symbol]
514
+ #
515
+ # @return [String]
516
+ #
517
+ def controller_for(key)
518
+ path = controllers[key] || DEFAULT_CONTROLLERS.fetch(key)
519
+ path.start_with?('/') ? path : "/#{path}"
134
520
  end
521
+
522
+ # Human-readable nickname for a repo key, used as the +to_param+ prefix on +PlanMyStuff::Issue+ instances. Falls
523
+ # back to +key.to_s.titleize+ when no explicit entry exists in +repo_nicknames+.
524
+ #
525
+ # @param key [Symbol, String]
526
+ #
527
+ # @return [String]
528
+ #
529
+ def repo_nickname_for(key)
530
+ repo_nicknames[key&.to_sym] || key.to_s.titleize
531
+ end
532
+
533
+ private
534
+
535
+ # Resolved nicknames feed directly into +Issue#to_param+ and route +:id+ tokens, so collisions or chars outside
536
+ # +[A-Za-z0-9_]+ (notably +-+, the nickname/number separator) break URL round-tripping.
537
+ #
538
+ # @raise [ConfigurationError] when any resolved nickname collides with another or contains non-token chars
539
+ #
540
+ # @return [void]
541
+ #
542
+ def validate_repo_nicknames!
543
+ resolved = repos.keys.index_with { |key| repo_nickname_for(key).to_s }
544
+
545
+ invalid = resolved.reject { |_key, nickname| nickname.match?(/\A[A-Za-z0-9_]+\z/) }
546
+ if invalid.present?
547
+ pairs = invalid.map { |key, nickname| "#{key.inspect} => #{nickname.inspect}" }
548
+ raise(
549
+ PlanMyStuff::ConfigurationError,
550
+ "Invalid repo nickname(s) (must match /\\A[A-Za-z0-9_]+\\z/): #{pairs.join(', ')}",
551
+ )
552
+ end
553
+
554
+ dupes = resolved.group_by { |_key, nickname| nickname }.select { |_n, entries| entries.size > 1 }
555
+ return if dupes.blank?
556
+
557
+ details = dupes.map do |nickname, entries|
558
+ keys = entries.map { |entry| entry.first.inspect }.join(', ')
559
+ "#{nickname.inspect} (#{keys})"
560
+ end
561
+ raise(
562
+ PlanMyStuff::ConfigurationError,
563
+ "Duplicate repo nickname(s): #{details.join('; ')}",
564
+ )
565
+ end
135
566
  end
136
567
 
137
568
  class ConfigurationError < StandardError