plan_my_stuff 0.11.0 → 0.13.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 +20 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +10 -0
- data/lib/plan_my_stuff/archive.rb +0 -2
- data/lib/plan_my_stuff/base_project.rb +0 -2
- data/lib/plan_my_stuff/configuration.rb +10 -0
- data/lib/plan_my_stuff/errors.rb +11 -0
- data/lib/plan_my_stuff/graphql/queries.rb +50 -0
- data/lib/plan_my_stuff/issue.rb +101 -7
- data/lib/plan_my_stuff/issue_field.rb +126 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +64 -0
- data/lib/plan_my_stuff/pipeline.rb +0 -5
- data/lib/plan_my_stuff/reminders.rb +0 -4
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff.rb +24 -37
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: caa7ca14656d70850ad30923738ffa58177caf5fb91bd14bc6551f547590ca63
|
|
4
|
+
data.tar.gz: 1d4b79cc957efa2c146ffb2a433a90c798680b23da7b8907d9351b5cb041cc09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd8f13707987c75445038cf93240be65897b00c538d3500665738554aa5ee3a6680352a2bb932e1fab41ba24e58985606655bd2f34e042375bc8a7692cde7310
|
|
7
|
+
data.tar.gz: 4b2160e17a7043004f294b7c6ae5c46f25f6a68ed5ca65ec5c3a124bfd34260521d1d07a6d22152e341f1d2ed8d6f1b79e729f6c9d39f8c7c0a907803baa017e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Internal: gem now autoloads `lib/plan_my_stuff/**` via Zeitwerk
|
|
8
|
+
(already shipped by railties - no new dep). All `require_relative`
|
|
9
|
+
boilerplate in the gem's lib tree is gone. The gem entry retains two
|
|
10
|
+
explicit `require_relative`s for files that fall outside the autoload
|
|
11
|
+
model: `errors.rb` (defines several sibling error classes) and
|
|
12
|
+
`engine.rb` (must register with Rails at load time).
|
|
13
|
+
|
|
14
|
+
## 0.12.0
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `PlanMyStuff::IssueField` for org-level GitHub Issue Field definitions (public preview).
|
|
19
|
+
Exposes `.list`, `.find` (case-insensitive), and `#option_id_for!` for resolving
|
|
20
|
+
single-select option names.
|
|
21
|
+
- `PlanMyStuff::Issue#issue_fields` returns a hash-like `IssueFieldValueSet` view of the
|
|
22
|
+
field values on an issue. Values are coerced to native types (`Date` for date fields,
|
|
23
|
+
`Float` for numbers, the option name `String` for single-selects).
|
|
24
|
+
- `PlanMyStuff::Issue#set_issue_fields!(updates)` writes one or more field values in a
|
|
25
|
+
single GraphQL mutation; passing `nil` as a value clears the field.
|
|
26
|
+
- `issue_fields:` kwarg on `Issue.create!`, `Issue.update!`, `Issue#save!`, and
|
|
27
|
+
`Issue#update!` so callers can set field values inline with create/update instead of
|
|
28
|
+
following up with an explicit `set_issue_fields!` call.
|
|
29
|
+
- `Configuration#issue_fields_enabled` (default `true`, opt-out). Set to `false` if your
|
|
30
|
+
org has not been admitted to the Issue Fields preview - with the flag off,
|
|
31
|
+
`Issue#issue_fields` returns an empty set and the write paths raise
|
|
32
|
+
`IssueFieldsNotEnabledError` instead of letting a raw GraphQL error surface.
|
|
33
|
+
|
|
3
34
|
## 0.11.0
|
|
4
35
|
|
|
5
36
|
### Added
|
data/CONFIGURATION.md
CHANGED
|
@@ -192,6 +192,26 @@ config.issue_types = {
|
|
|
192
192
|
}
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## Issue Fields (public preview)
|
|
196
|
+
|
|
197
|
+
| Option | Type | Default | Description |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `issue_fields_enabled` | `Boolean` | `true` | Whether the Issue Fields public preview is wired up for the org. |
|
|
200
|
+
|
|
201
|
+
GitHub Issue Fields are structured per-issue metadata (text, number, date, or single-select)
|
|
202
|
+
configured once at the org level. The preview is rolling out org-by-org. Leave this flag at its
|
|
203
|
+
default (`true`) once your org has been admitted; flip to `false` to keep the gem from issuing
|
|
204
|
+
calls that would otherwise return raw GraphQL errors.
|
|
205
|
+
|
|
206
|
+
With the flag off:
|
|
207
|
+
|
|
208
|
+
- `Issue#issue_fields` returns an empty `IssueFieldValueSet` without making a request.
|
|
209
|
+
- `Issue#set_issue_fields!(...)` and `IssueField.list` raise `IssueFieldsNotEnabledError`.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
config.issue_fields_enabled = false # org not admitted to the preview yet
|
|
213
|
+
```
|
|
214
|
+
|
|
195
215
|
## Release pipeline
|
|
196
216
|
|
|
197
217
|
| Option | Type | Default | Description |
|
|
@@ -168,6 +168,16 @@ PlanMyStuff.configure do |config|
|
|
|
168
168
|
# 'Feature' => 'Enhancement',
|
|
169
169
|
# }
|
|
170
170
|
|
|
171
|
+
# --------------------------------------------------------------------------
|
|
172
|
+
# Issue Fields (public preview)
|
|
173
|
+
# --------------------------------------------------------------------------
|
|
174
|
+
# GitHub Issue Fields is a per-org public preview. Default is true (opt-out).
|
|
175
|
+
# Flip to false if your org has not been admitted to the preview - read paths
|
|
176
|
+
# return an empty set and write paths raise IssueFieldsNotEnabledError instead
|
|
177
|
+
# of letting a raw GraphQL error bubble.
|
|
178
|
+
#
|
|
179
|
+
# config.issue_fields_enabled = false
|
|
180
|
+
|
|
171
181
|
# --------------------------------------------------------------------------
|
|
172
182
|
# Release pipeline
|
|
173
183
|
# --------------------------------------------------------------------------
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'base_project_extractions/graphql_hydration'
|
|
4
|
-
|
|
5
3
|
module PlanMyStuff
|
|
6
4
|
# Shared base for GitHub Projects V2 wrappers. Holds attribute definitions, generic find/list/update machinery,
|
|
7
5
|
# hydration, and instance helpers. Concrete subclasses (Project, TestingProject) add their own +create!+ behavior
|
|
@@ -356,6 +356,15 @@ module PlanMyStuff
|
|
|
356
356
|
#
|
|
357
357
|
attr_accessor :attachment_repo
|
|
358
358
|
|
|
359
|
+
# Whether GitHub's Issue Fields (public preview) are wired up for the configured org. Defaults to +true+ (opt-out):
|
|
360
|
+
# when +false+, +Issue#issue_fields+ returns an empty +IssueFieldValueSet+ without making a request and
|
|
361
|
+
# +Issue#set_issue_fields!+ / +IssueField.list+ raise +IssueFieldsNotEnabledError+. Set to +false+ if your org has
|
|
362
|
+
# not been admitted to the Issue Fields preview, to avoid raw GraphQL errors from GitHub.
|
|
363
|
+
#
|
|
364
|
+
# @return [Boolean]
|
|
365
|
+
#
|
|
366
|
+
attr_accessor :issue_fields_enabled
|
|
367
|
+
|
|
359
368
|
# @return [Configuration]
|
|
360
369
|
def initialize
|
|
361
370
|
@repos = {}
|
|
@@ -394,6 +403,7 @@ module PlanMyStuff
|
|
|
394
403
|
@archived_label = 'archived'
|
|
395
404
|
@pipeline_completion_purge_enabled = true
|
|
396
405
|
@pipeline_completion_ttl_hours = 24
|
|
406
|
+
@issue_fields_enabled = true
|
|
397
407
|
@process_aws_webhooks = Rails.env.production?
|
|
398
408
|
@sns_verifier_class = ::Aws::SNS::MessageVerifier if defined?(::Aws::SNS::MessageVerifier)
|
|
399
409
|
@sns_verifier_error =
|
data/lib/plan_my_stuff/errors.rb
CHANGED
|
@@ -106,6 +106,17 @@ module PlanMyStuff
|
|
|
106
106
|
class LockedIssueError < PlanMyStuff::Error
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Raised when an Issue Fields API call is attempted while
|
|
110
|
+
# +config.issue_fields_enabled+ is +false+. Consumers whose org has not been
|
|
111
|
+
# admitted to the Issue Fields public preview flip the flag off; this error
|
|
112
|
+
# surfaces faster (and with a clearer message) than the underlying
|
|
113
|
+
# +GraphQLError+ that GitHub would otherwise return.
|
|
114
|
+
class IssueFieldsNotEnabledError < PlanMyStuff::Error
|
|
115
|
+
def initialize(message = nil)
|
|
116
|
+
super(message || 'Issue Fields are disabled; set config.issue_fields_enabled = true to enable')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
109
120
|
# Raised by +PlanMyStuff::Pipeline+ forward transitions when the linked
|
|
110
121
|
# +Issue+ has any pending manager approvals.
|
|
111
122
|
class PendingApprovalsError < PlanMyStuff::ValidationError
|
|
@@ -399,6 +399,56 @@ module PlanMyStuff
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
GRAPHQL
|
|
402
|
+
|
|
403
|
+
# --- Issue Fields (org-level public preview) -----------------------
|
|
404
|
+
|
|
405
|
+
LIST_ORG_ISSUE_FIELDS = <<~GRAPHQL
|
|
406
|
+
query($org: String!) {
|
|
407
|
+
organization(login: $org) {
|
|
408
|
+
issueFields(first: 50) {
|
|
409
|
+
nodes {
|
|
410
|
+
__typename
|
|
411
|
+
... on IssueFieldText { id name description }
|
|
412
|
+
... on IssueFieldNumber { id name description }
|
|
413
|
+
... on IssueFieldDate { id name description }
|
|
414
|
+
... on IssueFieldSingleSelect {
|
|
415
|
+
id name description
|
|
416
|
+
options { id name description color }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
GRAPHQL
|
|
423
|
+
|
|
424
|
+
READ_ISSUE_FIELD_VALUES = <<~GRAPHQL
|
|
425
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
426
|
+
repository(owner: $owner, name: $name) {
|
|
427
|
+
issue(number: $number) {
|
|
428
|
+
issueFieldValues(first: 50) {
|
|
429
|
+
nodes {
|
|
430
|
+
__typename
|
|
431
|
+
... on IssueFieldTextValue { value field { ... on IssueFieldText { id name } } }
|
|
432
|
+
... on IssueFieldNumberValue { value field { ... on IssueFieldNumber { id name } } }
|
|
433
|
+
... on IssueFieldDateValue { value field { ... on IssueFieldDate { id name } } }
|
|
434
|
+
... on IssueFieldSingleSelectValue {
|
|
435
|
+
name optionId
|
|
436
|
+
field { ... on IssueFieldSingleSelect { id name } }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
GRAPHQL
|
|
444
|
+
|
|
445
|
+
SET_ISSUE_FIELD_VALUES = <<~GRAPHQL
|
|
446
|
+
mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!) {
|
|
447
|
+
setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields }) {
|
|
448
|
+
issue { number }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
GRAPHQL
|
|
402
452
|
end
|
|
403
453
|
end
|
|
404
454
|
end
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'issue_extractions/approvals'
|
|
4
|
-
require_relative 'issue_extractions/links'
|
|
5
|
-
require_relative 'issue_extractions/viewers'
|
|
6
|
-
require_relative 'issue_extractions/waiting'
|
|
7
|
-
|
|
8
3
|
module PlanMyStuff
|
|
9
4
|
# Wraps a GitHub issue with parsed PMS metadata and comments.
|
|
10
5
|
# Class methods provide the public API for CRUD operations.
|
|
@@ -84,6 +79,9 @@ module PlanMyStuff
|
|
|
84
79
|
# @param visibility_allowlist [Array<Integer>] user IDs for internal comment access
|
|
85
80
|
# @param issue_type [String, nil] GitHub issue type name (e.g. +"Bug"+, +"Feature"+). Must match a type
|
|
86
81
|
# configured on the org. +nil+ creates the issue with no type.
|
|
82
|
+
# @param issue_fields [Hash{String,Symbol => Object,nil}, nil] GitHub Issue Field values to apply after the
|
|
83
|
+
# issue is created. Delegates to +#set_issue_fields!+, so the same coercion rules and
|
|
84
|
+
# +IssueFieldsNotEnabledError+ behavior apply. +nil+ or an empty hash is a no-op.
|
|
87
85
|
# @param attachments [Array] files to upload to +config.attachment_repo+ and record on the body comment. Each
|
|
88
86
|
# entry may be an uploaded-file object responding to +#path+ and +#original_filename+ (e.g. Rails
|
|
89
87
|
# +ActionDispatch::Http::UploadedFile+), a String/Pathname path to a local file, or a pre-built
|
|
@@ -103,8 +101,13 @@ module PlanMyStuff
|
|
|
103
101
|
visibility: 'public',
|
|
104
102
|
visibility_allowlist: [],
|
|
105
103
|
issue_type: nil,
|
|
104
|
+
issue_fields: nil,
|
|
106
105
|
attachments: []
|
|
107
106
|
)
|
|
107
|
+
if issue_fields.present? && !PlanMyStuff.configuration.issue_fields_enabled
|
|
108
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError)
|
|
109
|
+
end
|
|
110
|
+
|
|
108
111
|
if body.blank?
|
|
109
112
|
raise(PlanMyStuff::ValidationError.new('body must be present', field: :body, expected_type: :string))
|
|
110
113
|
end
|
|
@@ -160,6 +163,8 @@ module PlanMyStuff
|
|
|
160
163
|
attachments: attachments,
|
|
161
164
|
)
|
|
162
165
|
|
|
166
|
+
issue.set_issue_fields!(issue_fields) if issue_fields.present?
|
|
167
|
+
|
|
163
168
|
issue.reload
|
|
164
169
|
PlanMyStuff::Notifications.instrument('issue.created', issue, user: user)
|
|
165
170
|
issue
|
|
@@ -187,6 +192,9 @@ module PlanMyStuff
|
|
|
187
192
|
# @param issue_type [String, nil] GitHub issue type name. Pass a String to set, +nil+ to clear, or omit the
|
|
188
193
|
# kwarg to leave the current type untouched. (+nil+-vs-omitted is differentiated by the private
|
|
189
194
|
# +ISSUE_TYPE_UNCHANGED+ sentinel.)
|
|
195
|
+
# @param issue_fields [Hash{String,Symbol => Object,nil}, nil] GitHub Issue Field values to apply after the
|
|
196
|
+
# PATCH (or instead of it, when no other attrs are provided). Delegates to +#set_issue_fields!+, so the same
|
|
197
|
+
# coercion rules and +IssueFieldsNotEnabledError+ behavior apply. +nil+ or an empty hash is a no-op.
|
|
190
198
|
#
|
|
191
199
|
# @return [Object]
|
|
192
200
|
#
|
|
@@ -199,7 +207,8 @@ module PlanMyStuff
|
|
|
199
207
|
labels: nil,
|
|
200
208
|
state: nil,
|
|
201
209
|
assignees: nil,
|
|
202
|
-
issue_type: ISSUE_TYPE_UNCHANGED
|
|
210
|
+
issue_type: ISSUE_TYPE_UNCHANGED,
|
|
211
|
+
issue_fields: nil
|
|
203
212
|
)
|
|
204
213
|
client = PlanMyStuff.client
|
|
205
214
|
resolved_repo = client.resolve_repo!(repo)
|
|
@@ -236,7 +245,8 @@ module PlanMyStuff
|
|
|
236
245
|
|
|
237
246
|
update_body_comment!(number, resolved_repo, body) if body
|
|
238
247
|
|
|
239
|
-
|
|
248
|
+
updated_issue = find(number, repo: resolved_repo).set_issue_fields!(issue_fields) if issue_fields.present?
|
|
249
|
+
return updated_issue if options.none?
|
|
240
250
|
|
|
241
251
|
result = client.rest(:update_issue, resolved_repo, number, **options)
|
|
242
252
|
store_etag_to_cache(client, resolved_repo, number, result, cache_writer: :write_issue)
|
|
@@ -557,6 +567,7 @@ module PlanMyStuff
|
|
|
557
567
|
visibility: metadata.visibility,
|
|
558
568
|
visibility_allowlist: Array.wrap(metadata.visibility_allowlist),
|
|
559
569
|
issue_type: issue_type,
|
|
570
|
+
issue_fields: @pending_issue_fields,
|
|
560
571
|
)
|
|
561
572
|
hydrate_from_issue(created)
|
|
562
573
|
else
|
|
@@ -565,6 +576,7 @@ module PlanMyStuff
|
|
|
565
576
|
instrument_update(captured_changes, user) unless skip_notification
|
|
566
577
|
end
|
|
567
578
|
|
|
579
|
+
@pending_issue_fields = nil
|
|
568
580
|
self
|
|
569
581
|
end
|
|
570
582
|
|
|
@@ -666,6 +678,42 @@ module PlanMyStuff
|
|
|
666
678
|
safe_read_field(github_response, :id)
|
|
667
679
|
end
|
|
668
680
|
|
|
681
|
+
# Returns a hash-like view of GitHub Issue Field values currently set on this issue. Reads on first access and
|
|
682
|
+
# memoizes; +set_issue_fields!+ invalidates the cache. Returns an empty set without making a request when
|
|
683
|
+
# +config.issue_fields_enabled+ is +false+.
|
|
684
|
+
#
|
|
685
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
686
|
+
#
|
|
687
|
+
def issue_fields
|
|
688
|
+
@issue_fields ||= load_issue_fields!
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Bulk-updates GitHub Issue Field values in a single +setIssueFieldValue+ mutation. Each key is the field display
|
|
692
|
+
# name; values are coerced to the right input fragment based on the field's type. Passing +nil+ as a value clears
|
|
693
|
+
# that field.
|
|
694
|
+
#
|
|
695
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError] when +config.issue_fields_enabled+ is +false+
|
|
696
|
+
# @raise [PlanMyStuff::Error] when a referenced field name does not exist on the org
|
|
697
|
+
#
|
|
698
|
+
# @param updates [Hash{String,Symbol => Object,nil}]
|
|
699
|
+
#
|
|
700
|
+
# @return [self]
|
|
701
|
+
#
|
|
702
|
+
def set_issue_fields!(updates)
|
|
703
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError) unless PlanMyStuff.configuration.issue_fields_enabled
|
|
704
|
+
|
|
705
|
+
fields_by_name = PlanMyStuff::IssueField.list(org: repo.organization).index_by { |field| field.name.downcase }
|
|
706
|
+
inputs = updates.map { |name, value| build_issue_field_input(fields_by_name, name, value) }
|
|
707
|
+
|
|
708
|
+
PlanMyStuff.client.graphql(
|
|
709
|
+
PlanMyStuff::GraphQL::Queries::SET_ISSUE_FIELD_VALUES,
|
|
710
|
+
variables: { issueId: github_node_id, issueFields: inputs },
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
@issue_fields = nil
|
|
714
|
+
self
|
|
715
|
+
end
|
|
716
|
+
|
|
669
717
|
private
|
|
670
718
|
|
|
671
719
|
# Populates this instance from a GitHub API response.
|
|
@@ -695,6 +743,7 @@ module PlanMyStuff
|
|
|
695
743
|
@body_dirty = false
|
|
696
744
|
persisted!
|
|
697
745
|
@comments = nil
|
|
746
|
+
@issue_fields = nil
|
|
698
747
|
invalidate_links_cache!
|
|
699
748
|
end
|
|
700
749
|
|
|
@@ -722,6 +771,7 @@ module PlanMyStuff
|
|
|
722
771
|
self.metadata = other.metadata
|
|
723
772
|
persisted!
|
|
724
773
|
@comments = nil
|
|
774
|
+
@issue_fields = nil
|
|
725
775
|
invalidate_links_cache!
|
|
726
776
|
end
|
|
727
777
|
|
|
@@ -764,6 +814,7 @@ module PlanMyStuff
|
|
|
764
814
|
}
|
|
765
815
|
attrs[:body] = body if @body_dirty
|
|
766
816
|
attrs[:assignees] = @pending_assignees unless @pending_assignees.nil?
|
|
817
|
+
attrs[:issue_fields] = @pending_issue_fields if @pending_issue_fields.present?
|
|
767
818
|
attrs[:issue_type] = issue_type if issue_type_changed?
|
|
768
819
|
|
|
769
820
|
clear_waiting_state_on_close(attrs)
|
|
@@ -789,6 +840,7 @@ module PlanMyStuff
|
|
|
789
840
|
self.body = attrs[:body] if attrs.key?(:body)
|
|
790
841
|
self.issue_type = attrs[:issue_type] if attrs.key?(:issue_type)
|
|
791
842
|
@pending_assignees = attrs[:assignees] if attrs.key?(:assignees)
|
|
843
|
+
@pending_issue_fields = attrs[:issue_fields] if attrs.key?(:issue_fields)
|
|
792
844
|
apply_metadata_attrs(attrs[:metadata]) if attrs.key?(:metadata)
|
|
793
845
|
end
|
|
794
846
|
|
|
@@ -905,5 +957,47 @@ module PlanMyStuff
|
|
|
905
957
|
|
|
906
958
|
id
|
|
907
959
|
end
|
|
960
|
+
|
|
961
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
962
|
+
def load_issue_fields!
|
|
963
|
+
return PlanMyStuff::IssueFieldValueSet.new({}) unless PlanMyStuff.configuration.issue_fields_enabled
|
|
964
|
+
|
|
965
|
+
data = PlanMyStuff.client.graphql(
|
|
966
|
+
PlanMyStuff::GraphQL::Queries::READ_ISSUE_FIELD_VALUES,
|
|
967
|
+
variables: { owner: repo.organization, name: repo.name, number: number },
|
|
968
|
+
)
|
|
969
|
+
nodes = data.dig(:repository, :issue, :issueFieldValues, :nodes)
|
|
970
|
+
PlanMyStuff::IssueFieldValueSet.from_graphql(nodes)
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Builds one element of the +issueFields+ argument to the +setIssueFieldValue+ mutation. Looks up the field
|
|
974
|
+
# definition in the provided hash to pick the right input fragment and (for single-select) resolve the option ID.
|
|
975
|
+
#
|
|
976
|
+
# @raise [PlanMyStuff::Error] if the field name is unknown on the org
|
|
977
|
+
#
|
|
978
|
+
# @param fields_by_name [Hash{String => PlanMyStuff::IssueField}] fields keyed by downcased display name
|
|
979
|
+
# @param name [String, Symbol]
|
|
980
|
+
# @param value [Object, nil]
|
|
981
|
+
#
|
|
982
|
+
# @return [Hash]
|
|
983
|
+
#
|
|
984
|
+
def build_issue_field_input(fields_by_name, name, value)
|
|
985
|
+
field = fields_by_name[name.to_s.downcase]
|
|
986
|
+
raise(PlanMyStuff::Error, "Unknown Issue Field #{name.inspect}") if field.nil?
|
|
987
|
+
|
|
988
|
+
return { fieldId: field.id, delete: true } if value.nil?
|
|
989
|
+
|
|
990
|
+
case field.type
|
|
991
|
+
when :single_select then { fieldId: field.id, singleSelectOptionId: field.option_id_for!(value) }
|
|
992
|
+
when :date then { fieldId: field.id, dateValue: value.to_date.iso8601 }
|
|
993
|
+
when :number
|
|
994
|
+
unless value.is_a?(Numeric)
|
|
995
|
+
raise(PlanMyStuff::Error, "Issue Field #{name.inspect} expects Numeric, got #{value.inspect}")
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
{ fieldId: field.id, numberValue: value.to_f }
|
|
999
|
+
when :text then { fieldId: field.id, textValue: value.to_s }
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
908
1002
|
end
|
|
909
1003
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Value object wrapping an organization-level GitHub Issue Field definition (public preview).
|
|
5
|
+
# Issue Fields are structured per-issue metadata (text, number, date, or single-select)
|
|
6
|
+
# configured once at the org level and applied across all of the org's repositories.
|
|
7
|
+
#
|
|
8
|
+
# Read-only on the gem side: callers manage field *definitions* through the GitHub UI, while
|
|
9
|
+
# the gem only handles field *values* on individual issues (see +Issue#issue_fields+).
|
|
10
|
+
class IssueField
|
|
11
|
+
# GraphQL +__typename+ -> normalized type symbol used internally.
|
|
12
|
+
TYPES = {
|
|
13
|
+
IssueFieldText: :text,
|
|
14
|
+
IssueFieldNumber: :number,
|
|
15
|
+
IssueFieldDate: :date,
|
|
16
|
+
IssueFieldSingleSelect: :single_select,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# @return [String] GraphQL node ID, e.g. +"IFSS_kgDOAAGskA"+
|
|
20
|
+
attr_reader :id
|
|
21
|
+
|
|
22
|
+
# @return [String] display name (e.g. +"Priority"+)
|
|
23
|
+
attr_reader :name
|
|
24
|
+
|
|
25
|
+
# @return [Symbol] one of +:text+, +:number+, +:date+, +:single_select+
|
|
26
|
+
attr_reader :type
|
|
27
|
+
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
attr_reader :description
|
|
30
|
+
|
|
31
|
+
# @return [Array<Hash>] for +:single_select+, the option list as returned by GraphQL with symbol keys
|
|
32
|
+
# (+id+, +name+, +description+, +color+). Empty for other field types.
|
|
33
|
+
attr_reader :options
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Lists Issue Field definitions configured on the org.
|
|
37
|
+
#
|
|
38
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError] if +config.issue_fields_enabled+ is +false+
|
|
39
|
+
#
|
|
40
|
+
# @param org [String, nil] org login; defaults to +config.organization+
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<PlanMyStuff::IssueField>]
|
|
43
|
+
#
|
|
44
|
+
def list(org: nil)
|
|
45
|
+
ensure_enabled!
|
|
46
|
+
|
|
47
|
+
org_login = org || PlanMyStuff.configuration.organization
|
|
48
|
+
data = PlanMyStuff.client.graphql(
|
|
49
|
+
PlanMyStuff::GraphQL::Queries::LIST_ORG_ISSUE_FIELDS,
|
|
50
|
+
variables: { org: org_login },
|
|
51
|
+
)
|
|
52
|
+
Array.wrap(data.dig(:organization, :issueFields, :nodes)).map { |node| from_graphql(node) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param name [String, Symbol]
|
|
56
|
+
# @param org [String, nil]
|
|
57
|
+
#
|
|
58
|
+
# @return [PlanMyStuff::IssueField, nil]
|
|
59
|
+
#
|
|
60
|
+
def find(name, org: nil)
|
|
61
|
+
list(org: org).find { |field| field.name.casecmp?(name.to_s) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param node [Hash] one node from +LIST_ORG_ISSUE_FIELDS+
|
|
65
|
+
#
|
|
66
|
+
# @return [PlanMyStuff::IssueField]
|
|
67
|
+
#
|
|
68
|
+
def from_graphql(node)
|
|
69
|
+
typename = node[:__typename]
|
|
70
|
+
type = TYPES[typename.to_sym] if typename
|
|
71
|
+
raise(PlanMyStuff::Error, "Unknown Issue Field typename: #{typename.inspect}") if type.nil?
|
|
72
|
+
|
|
73
|
+
new(
|
|
74
|
+
id: node.fetch(:id),
|
|
75
|
+
name: node.fetch(:name),
|
|
76
|
+
type: type,
|
|
77
|
+
description: node[:description],
|
|
78
|
+
options: Array.wrap(node[:options]),
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# @raise [PlanMyStuff::IssueFieldsNotEnabledError]
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
#
|
|
88
|
+
def ensure_enabled!
|
|
89
|
+
return if PlanMyStuff.configuration.issue_fields_enabled
|
|
90
|
+
|
|
91
|
+
raise(PlanMyStuff::IssueFieldsNotEnabledError)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param id [String]
|
|
96
|
+
# @param name [String]
|
|
97
|
+
# @param type [Symbol]
|
|
98
|
+
# @param description [String, nil]
|
|
99
|
+
# @param options [Array<Hash>]
|
|
100
|
+
#
|
|
101
|
+
def initialize(id:, name:, type:, description: nil, options: [])
|
|
102
|
+
@id = id
|
|
103
|
+
@name = name
|
|
104
|
+
@type = type
|
|
105
|
+
@description = description
|
|
106
|
+
@options = options
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Resolves a single-select option name to its GraphQL node ID.
|
|
110
|
+
#
|
|
111
|
+
# @raise [PlanMyStuff::Error] if this field is not a single-select, or the option name is unknown
|
|
112
|
+
#
|
|
113
|
+
# @param option_name [String, Symbol]
|
|
114
|
+
#
|
|
115
|
+
# @return [String]
|
|
116
|
+
#
|
|
117
|
+
def option_id_for!(option_name)
|
|
118
|
+
raise(PlanMyStuff::Error, "Field #{name.inspect} is not a single-select") unless type == :single_select
|
|
119
|
+
|
|
120
|
+
match = options.find { |option| option.fetch(:name).casecmp?(option_name.to_s) }
|
|
121
|
+
raise(PlanMyStuff::Error, "Unknown option #{option_name.inspect} for field #{name.inspect}") if match.nil?
|
|
122
|
+
|
|
123
|
+
match.fetch(:id)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Hash-like read-side view of GitHub Issue Field values on a single +Issue+. Returned by
|
|
5
|
+
# +Issue#issue_fields+. Values are coerced into Ruby types on construction: date fields come back
|
|
6
|
+
# as +Date+, number fields as +Float+, single-select fields as the option name +String+, and
|
|
7
|
+
# text fields as the raw +String+.
|
|
8
|
+
#
|
|
9
|
+
# Access is by field display name; string and symbol keys both work. Iteration yields
|
|
10
|
+
# +[name, value]+ pairs in the order GitHub returned them.
|
|
11
|
+
class IssueFieldValueSet
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
delegate :empty?, to: :@hash
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# @param nodes [Array<Hash>, nil] +issueFieldValues.nodes+ from the GraphQL read query
|
|
18
|
+
#
|
|
19
|
+
# @return [PlanMyStuff::IssueFieldValueSet]
|
|
20
|
+
#
|
|
21
|
+
def from_graphql(nodes)
|
|
22
|
+
pairs = Array.wrap(nodes).map { |node| [node.dig(:field, :name), coerce(node)] }
|
|
23
|
+
new(pairs.to_h)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param node [Hash]
|
|
27
|
+
#
|
|
28
|
+
# @return [Object]
|
|
29
|
+
#
|
|
30
|
+
def coerce(node)
|
|
31
|
+
case node[:__typename].to_s
|
|
32
|
+
when 'IssueFieldDateValue' then Date.parse(node.fetch(:value))
|
|
33
|
+
when 'IssueFieldNumberValue' then node.fetch(:value).to_f
|
|
34
|
+
when 'IssueFieldSingleSelectValue' then node.fetch(:name)
|
|
35
|
+
else node.fetch(:value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param hash [Hash{String => Object}]
|
|
41
|
+
#
|
|
42
|
+
def initialize(hash)
|
|
43
|
+
@hash = hash
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param name [String, Symbol] field display name
|
|
47
|
+
#
|
|
48
|
+
# @return [Object, nil]
|
|
49
|
+
#
|
|
50
|
+
def [](name)
|
|
51
|
+
@hash[name.to_s]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Hash{String => Object}] copy of the underlying hash
|
|
55
|
+
def to_h
|
|
56
|
+
@hash.dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Enumerator, void]
|
|
60
|
+
def each(&)
|
|
61
|
+
@hash.each(&)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'pipeline/completed_sweep'
|
|
4
|
-
require_relative 'pipeline/issue_linker'
|
|
5
|
-
require_relative 'pipeline/status'
|
|
6
|
-
require_relative 'pipeline/testing'
|
|
7
|
-
|
|
8
3
|
module PlanMyStuff
|
|
9
4
|
# High-level orchestration layer for the release pipeline.
|
|
10
5
|
#
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -1,48 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
|
|
3
5
|
require 'date'
|
|
4
6
|
|
|
5
7
|
require 'active_support/core_ext/array/wrap'
|
|
6
8
|
require 'active_support/core_ext/object/blank'
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
loader = Zeitwerk::Loader.for_gem
|
|
11
|
+
loader.inflector.inflect(
|
|
12
|
+
'graphql' => 'GraphQL',
|
|
13
|
+
'version' => 'VERSION',
|
|
14
|
+
)
|
|
15
|
+
loader.ignore(
|
|
16
|
+
File.join(__dir__, 'generators'),
|
|
17
|
+
File.join(__dir__, 'tasks'),
|
|
18
|
+
File.join(__dir__, 'plan_my_stuff', 'aws_sns_simulator.rb'),
|
|
19
|
+
File.join(__dir__, 'plan_my_stuff', 'engine.rb'),
|
|
20
|
+
File.join(__dir__, 'plan_my_stuff', 'errors.rb'),
|
|
21
|
+
File.join(__dir__, 'plan_my_stuff', 'test_helpers.rb'),
|
|
22
|
+
File.join(__dir__, 'plan_my_stuff', 'webhook_replayer.rb'),
|
|
23
|
+
)
|
|
24
|
+
loader.setup
|
|
25
|
+
|
|
26
|
+
# errors.rb defines several sibling error classes - load it eagerly rather
|
|
27
|
+
# than rely on autoload-via-lead-constant.
|
|
24
28
|
require_relative 'plan_my_stuff/errors'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
require_relative 'plan_my_stuff/
|
|
29
|
-
require_relative 'plan_my_stuff/link'
|
|
30
|
-
require_relative 'plan_my_stuff/markdown'
|
|
31
|
-
require_relative 'plan_my_stuff/metadata_parser'
|
|
32
|
-
require_relative 'plan_my_stuff/notifications'
|
|
33
|
-
require_relative 'plan_my_stuff/pipeline'
|
|
34
|
-
require_relative 'plan_my_stuff/project'
|
|
35
|
-
require_relative 'plan_my_stuff/project_item'
|
|
36
|
-
require_relative 'plan_my_stuff/project_item_metadata'
|
|
37
|
-
require_relative 'plan_my_stuff/project_metadata'
|
|
38
|
-
require_relative 'plan_my_stuff/reminders'
|
|
39
|
-
require_relative 'plan_my_stuff/repo'
|
|
40
|
-
require_relative 'plan_my_stuff/testing_project'
|
|
41
|
-
require_relative 'plan_my_stuff/testing_project_item'
|
|
42
|
-
require_relative 'plan_my_stuff/testing_project_metadata'
|
|
43
|
-
require_relative 'plan_my_stuff/user_resolver'
|
|
44
|
-
require_relative 'plan_my_stuff/verifier'
|
|
45
|
-
require_relative 'plan_my_stuff/version'
|
|
29
|
+
|
|
30
|
+
# Engine must register with Rails at load time, so eagerly require it
|
|
31
|
+
# (and only when Rails is defined - otherwise Rails::Engine is undefined).
|
|
32
|
+
require_relative 'plan_my_stuff/engine' if defined?(Rails)
|
|
46
33
|
|
|
47
34
|
module PlanMyStuff
|
|
48
35
|
class << self
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plan_my_stuff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
@@ -132,6 +132,8 @@ files:
|
|
|
132
132
|
- lib/plan_my_stuff/issue_extractions/links.rb
|
|
133
133
|
- lib/plan_my_stuff/issue_extractions/viewers.rb
|
|
134
134
|
- lib/plan_my_stuff/issue_extractions/waiting.rb
|
|
135
|
+
- lib/plan_my_stuff/issue_field.rb
|
|
136
|
+
- lib/plan_my_stuff/issue_field_value_set.rb
|
|
135
137
|
- lib/plan_my_stuff/issue_metadata.rb
|
|
136
138
|
- lib/plan_my_stuff/label.rb
|
|
137
139
|
- lib/plan_my_stuff/link.rb
|