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 +4 -4
- data/CHANGELOG.md +31 -0
- data/CONFIGURATION.md +351 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +1 -1
- data/app/views/plan_my_stuff/partials/_flash.html.erb +0 -1
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +1 -12
- data/lib/plan_my_stuff/base_project.rb +5 -176
- data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +184 -0
- data/lib/plan_my_stuff/base_project_item.rb +1 -0
- data/lib/plan_my_stuff/comment.rb +5 -3
- data/lib/plan_my_stuff/configuration.rb +3 -16
- data/lib/plan_my_stuff/issue.rb +15 -1082
- data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
- data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
- data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
- data/lib/plan_my_stuff/issue_extractions/waiting.rb +148 -0
- data/lib/plan_my_stuff/label.rb +4 -4
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/tasks/plan_my_stuff.rake +2 -2
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d74ab84a6dc7a2e69a33df3e7c1babc51e8e0e9813b388b541b5021c7a61169
|
|
4
|
+
data.tar.gz: 492a5c2be1f50cb1fe899d101dee7157a779ff649c292e0075fbe89cc5f8cd55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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('
|
|
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)) %>
|
|
@@ -106,19 +106,8 @@ PlanMyStuff.configure do |config|
|
|
|
106
106
|
# "#{url_options[:protocol] || 'http'}://#{url_options[:host]}/issues"
|
|
107
107
|
|
|
108
108
|
# --------------------------------------------------------------------------
|
|
109
|
-
#
|
|
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
|
|