plan_my_stuff 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6799008cb353423175bdd3132e0fa5f14981b3710378c3b0886a8e9a2394b610
4
- data.tar.gz: 75b8336ec9aabd1d77a2642de77c9e46310dda3da49e6abd97b997927e906d05
3
+ metadata.gz: 3d74ab84a6dc7a2e69a33df3e7c1babc51e8e0e9813b388b541b5021c7a61169
4
+ data.tar.gz: 492a5c2be1f50cb1fe899d101dee7157a779ff649c292e0075fbe89cc5f8cd55
5
5
  SHA512:
6
- metadata.gz: fdb5b8790905ff4f529496ad2a5c63b13a59a58ce524a8df79b20b0efc536854bc986f0e2fe7896b77bfe9b0a15598b6212a83991d46c9925e4dbf9eecab4871
7
- data.tar.gz: b5c150efa84a7ed3143935216d0ac52ae9c8fa7d99cb45b6add6552c13f3b66fad2f604e1aa8871cb05fc69b3aa16e650bce14a60ad1490d0650af3a3e646d91
6
+ metadata.gz: 2838c1763d069dc702b99e3d89591b8bc9bec52c2d686ccef1e85ceefa3d9d1fbfe90b0b1875d6d200ec6a88236d5e2619d6bf2151e73b075dfca7b693e2ef0c
7
+ data.tar.gz: c42b1d88a9fbbc07c1c8f7db547af43a9b3417858e0a542c6a0cc1ae2addf76fb44630a9675b5ab6f1999e26330822fdf5500245cbedb08946e3da5029086f93
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Changed
6
+
7
+ - `Issue` slimmed from 1791 to 911 lines by extracting feature clusters into per-feature modules under a sibling
8
+ `PlanMyStuff::IssueExtractions::*` namespace, included into `Issue`. Public API unchanged (`issue.approve!`,
9
+ `issue.add_related!`, `issue.enter_waiting_on_user!`, `issue.add_viewers!`, etc. still resolve to the same methods).
10
+ - `PlanMyStuff::IssueExtractions::Approvals` - `lib/plan_my_stuff/issue_extractions/approvals.rb`
11
+ - `PlanMyStuff::IssueExtractions::Links` - `lib/plan_my_stuff/issue_extractions/links.rb`
12
+ - `PlanMyStuff::IssueExtractions::Waiting` - `lib/plan_my_stuff/issue_extractions/waiting.rb`
13
+ - `PlanMyStuff::IssueExtractions::Viewers` - `lib/plan_my_stuff/issue_extractions/viewers.rb`
14
+ - `BaseProject` slimmed from 661 to 504 lines by extracting GraphQL hydration helpers into
15
+ `PlanMyStuff::BaseProjectExtractions::GraphqlHydration` (`lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb`),
16
+ included into `BaseProject`'s singleton class. `BaseProject.find` / `BaseProject.list` remain public entry points.
17
+ - Specs for `Issue` features now live alongside the modules at `spec/plan_my_stuff/issue_extractions/<feature>_spec.rb`.
18
+
19
+ ## 0.9.0
20
+
21
+ ### Breaking
22
+
23
+ - `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
24
+
25
+ ### Added
26
+
27
+ - `CONFIGURATION.md` is now available bundled with the gem
28
+
29
+ ### Changed
30
+
31
+ - `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
32
+ - `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
33
+
3
34
  ## 0.8.0
4
35
 
5
36
  ### Breaking
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` |
@@ -21,7 +21,7 @@
21
21
  <%= button_to('Take', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
22
22
  <% end %>
23
23
  <% if @support_user && @pipeline_enabled && @current_user_login.present? && @issue.assignees.include?(@current_user_login) %>
24
- <%= button_to('Release', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
24
+ <%= button_to('Unassign', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
25
25
  <% end %>
26
26
  <% if @support_user %>
27
27
  <%= link_to('Start Testing Project', plan_my_stuff.new_testing_project_path(subject_url: @issue.html_url)) %>
@@ -1,4 +1,3 @@
1
- <%# locals: () %>
2
1
  <% if flash[:error].present? %>
3
2
  <p role="alert" style="color: red;"><%= flash[:error] %></p>
4
3
  <% end %>
@@ -106,19 +106,8 @@ PlanMyStuff.configure do |config|
106
106
  # "#{url_options[:protocol] || 'http'}://#{url_options[:host]}/issues"
107
107
 
108
108
  # --------------------------------------------------------------------------
109
- # Request gateway
109
+ # Notification actor
110
110
  # --------------------------------------------------------------------------
111
- # Proc returning boolean, or nil (always send). When it returns false the
112
- # request is deferred to a background job instead of hitting GitHub.
113
- # config.should_send_request = -> { !MaintenanceMode.active? }
114
-
115
- # Map of action type to job class name for deferred requests.
116
- # config.job_classes = {
117
- # create_ticket: 'PmsCreateTicketJob',
118
- # post_comment: 'PmsPostCommentJob',
119
- # update_status: 'PmsUpdateStatusJob'
120
- # }
121
-
122
111
  # Fallback actor for notification events (plan_my_stuff.*) when a caller
123
112
  # does not pass an explicit user: kwarg. Proc/lambda called at event time.
124
113
  # config.current_user = -> { Current.user }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'base_project_extractions/graphql_hydration'
4
+
3
5
  module PlanMyStuff
4
6
  # Shared base for GitHub Projects V2 wrappers. Holds attribute definitions, generic find/list/update machinery,
5
7
  # hydration, and instance helpers. Concrete subclasses (Project, TestingProject) add their own +create!+ behavior
@@ -43,6 +45,8 @@ module PlanMyStuff
43
45
  attribute :has_next_page
44
46
 
45
47
  class << self
48
+ include PlanMyStuff::BaseProjectExtractions::GraphqlHydration
49
+
46
50
  # Generic find - returns whichever concrete project type is at the given number, dispatching on metadata kind.
47
51
  # Subclasses may override to apply filtering (e.g. Project raises for testing projects by default).
48
52
  #
@@ -180,47 +184,6 @@ module PlanMyStuff
180
184
 
181
185
  private
182
186
 
183
- # Builds a summary Project from a list query node. Dispatches to TestingProject when the readme metadata has
184
- # kind: "testing".
185
- #
186
- # @param node [Hash]
187
- #
188
- # @return [PlanMyStuff::BaseProject]
189
- #
190
- def build_summary(node)
191
- raw_readme = node[:readme] || ''
192
- parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
193
- klass = dispatch_project_class(parsed_meta[:metadata])
194
- project = klass.new
195
- project.__send__(:hydrate_summary, node, raw_readme: raw_readme, parsed_meta: parsed_meta)
196
- project
197
- end
198
-
199
- # Builds a detailed Project from a find query response. Dispatches to TestingProject when the readme metadata
200
- # has kind: "testing".
201
- #
202
- # @param graphql_project [Hash]
203
- # @param items [Array<Hash>]
204
- # @param next_cursor [String, nil]
205
- # @param has_next_page [Boolean, nil]
206
- #
207
- # @return [PlanMyStuff::BaseProject]
208
- #
209
- def build_detail(graphql_project, items:, next_cursor: nil, has_next_page: nil)
210
- raw_readme = graphql_project[:readme] || ''
211
- parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
212
- klass = dispatch_project_class(parsed_meta[:metadata])
213
- project = klass.new
214
- project.__send__(
215
- :hydrate_detail,
216
- graphql_project,
217
- items: items,
218
- next_cursor: next_cursor,
219
- has_next_page: has_next_page,
220
- )
221
- project
222
- end
223
-
224
187
  # Returns the appropriate project class based on the metadata kind field. Always dispatches to a concrete
225
188
  # subclass (never BaseProject itself).
226
189
  #
@@ -234,140 +197,6 @@ module PlanMyStuff
234
197
  PlanMyStuff::Project
235
198
  end
236
199
 
237
- # @param org [String]
238
- # @param number [Integer]
239
- #
240
- # @return [PlanMyStuff::BaseProject]
241
- #
242
- def find_auto_paginated(org, number)
243
- all_items = []
244
- cursor = nil
245
- raw_project = nil
246
- page = nil
247
-
248
- loop do
249
- page = fetch_project_page(org, number, cursor)
250
- raw_project ||= page[:raw]
251
- all_items.concat(page[:items])
252
-
253
- break if !page[:has_next_page] || all_items.length >= MAX_AUTO_PAGINATE_ITEMS
254
-
255
- cursor = page[:next_cursor]
256
- end
257
-
258
- build_detail(
259
- raw_project,
260
- items: all_items,
261
- next_cursor: page[:next_cursor],
262
- has_next_page: page[:has_next_page],
263
- )
264
- end
265
-
266
- # @param org [String]
267
- # @param number [Integer]
268
- # @param cursor [String, nil]
269
- #
270
- # @return [PlanMyStuff::BaseProject]
271
- #
272
- def find_with_cursor(org, number, cursor:)
273
- page = fetch_project_page(org, number, cursor)
274
- build_detail(
275
- page[:raw],
276
- items: page[:items],
277
- next_cursor: page[:next_cursor],
278
- has_next_page: page[:has_next_page],
279
- )
280
- end
281
-
282
- # Fetches a single page of project data. Returns a lightweight hash for pagination loop consumption (not a
283
- # Project instance).
284
- #
285
- # @param org [String]
286
- # @param number [Integer]
287
- # @param cursor [String, nil]
288
- #
289
- # @return [Hash] with :raw, :items, :next_cursor, :has_next_page
290
- #
291
- def fetch_project_page(org, number, cursor)
292
- variables = { org: org, number: number }
293
- variables[:cursor] = cursor if cursor
294
-
295
- data = PlanMyStuff.client.graphql(
296
- PlanMyStuff::GraphQL::Queries::FIND_PROJECT,
297
- variables: variables,
298
- )
299
-
300
- raw_project = data.dig(:organization, :projectV2)
301
- page_info = raw_project.dig(:items, :pageInfo) || {}
302
- items_data = raw_project.dig(:items, :nodes) || []
303
-
304
- {
305
- raw: raw_project,
306
- items: items_data.map { |item| parse_project_item(item) },
307
- next_cursor: page_info[:endCursor],
308
- has_next_page: page_info[:hasNextPage],
309
- }
310
- end
311
-
312
- # @param item [Hash] raw GraphQL project item node
313
- #
314
- # @return [Hash]
315
- #
316
- def parse_project_item(item)
317
- content = item[:content] || {}
318
- field_values = item.dig(:fieldValues, :nodes) || []
319
- repo_name = content.dig(:repository, :nameWithOwner)
320
-
321
- {
322
- id: item[:id],
323
- type: item[:type],
324
- content_node_id: content[:id],
325
- title: content[:title],
326
- body: content[:body],
327
- number: content[:number],
328
- url: content[:url],
329
- state: content[:state],
330
- repo: repo_name.present? ? PlanMyStuff::Repo.resolve!(repo_name) : nil,
331
- status: extract_item_status(field_values),
332
- field_values: parse_field_values(field_values),
333
- updated_at: item[:updatedAt],
334
- github_response: item,
335
- }
336
- end
337
-
338
- # @param field_values [Array<Hash>]
339
- #
340
- # @return [String, nil]
341
- #
342
- def extract_item_status(field_values)
343
- status_value = field_values.find { |fv| fv.dig(:field, :name) == 'Status' }
344
-
345
- status_value&.dig(:name)
346
- end
347
-
348
- # @param field_values [Array<Hash>]
349
- #
350
- # @return [Hash]
351
- #
352
- def parse_field_values(field_values)
353
- result = {}
354
-
355
- field_values.each do |fv|
356
- field_name = fv.dig(:field, :name)
357
- next unless field_name
358
-
359
- value = fv[:name] || fv[:text]
360
- users_node = fv[:users]
361
- if users_node
362
- value = (users_node[:nodes] || []).map { |u| u[:login] }
363
- end
364
-
365
- result[field_name] = value
366
- end
367
-
368
- result
369
- end
370
-
371
200
  # Resolves a project number to its node ID.
372
201
  #
373
202
  # @param org [String]
@@ -406,7 +235,7 @@ module PlanMyStuff
406
235
  #
407
236
  def status_field
408
237
  status_field!
409
- rescue
238
+ rescue PlanMyStuff::APIError
410
239
  nil
411
240
  end
412
241