plan_my_stuff 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +595 -0
- data/CONFIGURATION.md +487 -0
- data/README.md +612 -88
- data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
- data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
- data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
- data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
- data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
- data/app/jobs/plan_my_stuff/application_job.rb +8 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
- data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
- data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +43 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
- data/lib/plan_my_stuff/application_record.rb +158 -1
- data/lib/plan_my_stuff/approval.rb +88 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +12 -0
- data/lib/plan_my_stuff/attachment.rb +83 -0
- data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
- data/lib/plan_my_stuff/base_metadata.rb +25 -28
- data/lib/plan_my_stuff/base_project.rb +502 -0
- data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
- data/lib/plan_my_stuff/base_project_item.rb +588 -0
- data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
- data/lib/plan_my_stuff/cache.rb +197 -0
- data/lib/plan_my_stuff/client.rb +139 -64
- data/lib/plan_my_stuff/comment.rb +225 -100
- data/lib/plan_my_stuff/comment_metadata.rb +68 -5
- data/lib/plan_my_stuff/configuration.rb +459 -28
- data/lib/plan_my_stuff/custom_fields.rb +96 -12
- data/lib/plan_my_stuff/engine.rb +14 -2
- data/lib/plan_my_stuff/errors.rb +65 -5
- data/lib/plan_my_stuff/graphql/queries.rb +454 -0
- data/lib/plan_my_stuff/issue.rb +1097 -166
- 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 +171 -0
- data/lib/plan_my_stuff/issue_field.rb +126 -0
- data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
- data/lib/plan_my_stuff/issue_metadata.rb +132 -21
- data/lib/plan_my_stuff/label.rb +100 -13
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/markdown.rb +13 -7
- data/lib/plan_my_stuff/metadata_parser.rb +51 -12
- data/lib/plan_my_stuff/notifications.rb +148 -0
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +40 -0
- data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
- data/lib/plan_my_stuff/pipeline.rb +310 -0
- data/lib/plan_my_stuff/project.rb +63 -465
- data/lib/plan_my_stuff/project_item.rb +3 -409
- data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
- data/lib/plan_my_stuff/project_metadata.rb +47 -0
- data/lib/plan_my_stuff/reminders/closer.rb +70 -0
- data/lib/plan_my_stuff/reminders/fire.rb +129 -0
- data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
- data/lib/plan_my_stuff/reminders.rb +12 -0
- data/lib/plan_my_stuff/repo.rb +145 -0
- data/lib/plan_my_stuff/test_helpers.rb +265 -25
- data/lib/plan_my_stuff/testing_project.rb +292 -0
- data/lib/plan_my_stuff/testing_project_item.rb +218 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +24 -3
- data/lib/plan_my_stuff/verifier.rb +10 -0
- data/lib/plan_my_stuff/version.rb +2 -2
- data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
- data/lib/plan_my_stuff.rb +55 -20
- data/lib/tasks/plan_my_stuff.rake +331 -0
- metadata +99 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# ETag-backed HTTP cache for GitHub REST reads.
|
|
5
|
+
#
|
|
6
|
+
# Stores `{ etag:, body: }` pairs in `Rails.cache` keyed by resource.
|
|
7
|
+
# Readers look up the entry, pass its ETag in `If-None-Match`, and
|
|
8
|
+
# either (a) get a cheap 304 and serve the cached body, or (b) get a
|
|
9
|
+
# fresh 200, write the new ETag + body, and return that. Writes from
|
|
10
|
+
# +.create!+/+.update!+ front-load the cache with the fresh ETag
|
|
11
|
+
# returned on the mutating response so the next read is cheap too.
|
|
12
|
+
#
|
|
13
|
+
# Covers issues, comments, and their list endpoints.
|
|
14
|
+
#
|
|
15
|
+
module Cache
|
|
16
|
+
# Gem-internal cache version. Bump when the cache layout or the
|
|
17
|
+
# payload shape changes in a backwards-incompatible way so existing
|
|
18
|
+
# entries are orphaned rather than mis-read.
|
|
19
|
+
CACHE_VERSION = 'v1'
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @return [Boolean] whether `Rails.cache` is available and caching is enabled
|
|
23
|
+
def enabled?
|
|
24
|
+
return false unless PlanMyStuff.configuration.cache_enabled
|
|
25
|
+
return false if !defined?(::Rails) || !::Rails.respond_to?(:cache) || ::Rails.cache.nil?
|
|
26
|
+
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Reads the cached `{etag:, body:}` entry for an issue.
|
|
31
|
+
#
|
|
32
|
+
# @param repo [String] resolved full repo name (e.g. "Org/Name")
|
|
33
|
+
# @param number [Integer]
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash{Symbol => Object}, nil]
|
|
36
|
+
#
|
|
37
|
+
def read_issue(repo, number)
|
|
38
|
+
return unless enabled?
|
|
39
|
+
|
|
40
|
+
::Rails.cache.read(issue_key(repo, number))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Writes a cache entry for an issue.
|
|
44
|
+
#
|
|
45
|
+
# @param repo [String] resolved full repo name
|
|
46
|
+
# @param number [Integer]
|
|
47
|
+
# @param etag [String, nil] value of the `ETag` response header
|
|
48
|
+
# @param body [Object] parsed GitHub issue response
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
#
|
|
52
|
+
def write_issue(repo, number, etag:, body:)
|
|
53
|
+
return unless enabled?
|
|
54
|
+
return if etag.blank?
|
|
55
|
+
|
|
56
|
+
::Rails.cache.write(
|
|
57
|
+
issue_key(repo, number),
|
|
58
|
+
{ etag: etag, body: normalize(body) },
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Removes the cache entry for an issue.
|
|
63
|
+
#
|
|
64
|
+
# @param repo [String]
|
|
65
|
+
# @param number [Integer]
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
#
|
|
69
|
+
def delete_issue(repo, number)
|
|
70
|
+
return unless enabled?
|
|
71
|
+
|
|
72
|
+
::Rails.cache.delete(issue_key(repo, number))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reads the cached `{etag:, body:}` entry for a comment.
|
|
76
|
+
#
|
|
77
|
+
# @param repo [String] resolved full repo name (e.g. "Org/Name")
|
|
78
|
+
# @param id [Integer]
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash{Symbol => Object}, nil]
|
|
81
|
+
#
|
|
82
|
+
def read_comment(repo, id)
|
|
83
|
+
return unless enabled?
|
|
84
|
+
|
|
85
|
+
::Rails.cache.read(comment_key(repo, id))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Writes a cache entry for a comment.
|
|
89
|
+
#
|
|
90
|
+
# @param repo [String] resolved full repo name
|
|
91
|
+
# @param id [Integer]
|
|
92
|
+
# @param etag [String, nil] value of the `ETag` response header
|
|
93
|
+
# @param body [Object] parsed GitHub comment response
|
|
94
|
+
#
|
|
95
|
+
# @return [void]
|
|
96
|
+
#
|
|
97
|
+
def write_comment(repo, id, etag:, body:)
|
|
98
|
+
return unless enabled?
|
|
99
|
+
return if etag.blank?
|
|
100
|
+
|
|
101
|
+
::Rails.cache.write(
|
|
102
|
+
comment_key(repo, id),
|
|
103
|
+
{ etag: etag, body: normalize(body) },
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Removes the cache entry for a comment.
|
|
108
|
+
#
|
|
109
|
+
# @param repo [String]
|
|
110
|
+
# @param id [Integer]
|
|
111
|
+
#
|
|
112
|
+
# @return [void]
|
|
113
|
+
#
|
|
114
|
+
def delete_comment(repo, id)
|
|
115
|
+
return unless enabled?
|
|
116
|
+
|
|
117
|
+
::Rails.cache.delete(comment_key(repo, id))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Reads the cached `{etag:, body:}` entry for a list endpoint.
|
|
121
|
+
#
|
|
122
|
+
# @param resource [Symbol] :issue or :comment
|
|
123
|
+
# @param repo [String] resolved full repo name
|
|
124
|
+
# @param params [Hash] query params that make this list unique
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash{Symbol => Object}, nil]
|
|
127
|
+
#
|
|
128
|
+
def read_list(resource, repo, params)
|
|
129
|
+
return unless enabled?
|
|
130
|
+
|
|
131
|
+
::Rails.cache.read(list_key(resource, repo, params))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Writes a cache entry for a list endpoint.
|
|
135
|
+
#
|
|
136
|
+
# @param resource [Symbol] :issue or :comment
|
|
137
|
+
# @param repo [String] resolved full repo name
|
|
138
|
+
# @param params [Hash] query params that make this list unique
|
|
139
|
+
# @param etag [String, nil] value of the `ETag` response header
|
|
140
|
+
# @param body [Object] parsed GitHub list response (array of resources)
|
|
141
|
+
#
|
|
142
|
+
# @return [void]
|
|
143
|
+
#
|
|
144
|
+
def write_list(resource, repo, params, etag:, body:)
|
|
145
|
+
return unless enabled?
|
|
146
|
+
return if etag.blank?
|
|
147
|
+
|
|
148
|
+
::Rails.cache.write(
|
|
149
|
+
list_key(resource, repo, params),
|
|
150
|
+
{ etag: etag, body: normalize(body) },
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# @return [String]
|
|
157
|
+
def issue_key(repo, number)
|
|
158
|
+
[key_prefix, 'issue', repo, number].join('/')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [String]
|
|
162
|
+
def comment_key(repo, id)
|
|
163
|
+
[key_prefix, 'comment', repo, id].join('/')
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [String]
|
|
167
|
+
def list_key(resource, repo, params)
|
|
168
|
+
serialized = params.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}:#{v}" }.join(',')
|
|
169
|
+
[key_prefix, 'list', resource, repo, serialized].join('/')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Builds the cache key prefix from gem + app cache versions so
|
|
173
|
+
# that bumping either side orphans all existing entries.
|
|
174
|
+
#
|
|
175
|
+
# @return [String]
|
|
176
|
+
#
|
|
177
|
+
def key_prefix
|
|
178
|
+
app_version = PlanMyStuff.configuration.cache_version || '0'
|
|
179
|
+
['pms', CACHE_VERSION, app_version].join('/')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Coerces a Sawyer::Resource, Struct, Array, or similar to a
|
|
183
|
+
# plain Hash (or Array of Hashes) so the cache payload is
|
|
184
|
+
# portable across stores. Anything else is passed through.
|
|
185
|
+
#
|
|
186
|
+
# @return [Object]
|
|
187
|
+
#
|
|
188
|
+
def normalize(body)
|
|
189
|
+
return body.map { |item| normalize(item) } if body.is_a?(Array)
|
|
190
|
+
return body.to_hash if body.respond_to?(:to_hash)
|
|
191
|
+
return body.to_h if body.respond_to?(:to_h)
|
|
192
|
+
|
|
193
|
+
body
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/plan_my_stuff/client.rb
CHANGED
|
@@ -4,18 +4,63 @@ require 'active_support/core_ext/hash/keys'
|
|
|
4
4
|
require 'octokit'
|
|
5
5
|
|
|
6
6
|
module PlanMyStuff
|
|
7
|
-
# Infrastructure wrapper around Octokit. Handles auth, error normalization,
|
|
8
|
-
#
|
|
9
|
-
# internally via PlanMyStuff.client.
|
|
7
|
+
# Infrastructure wrapper around Octokit. Handles auth, error normalization, and repo resolution. Domain modules
|
|
8
|
+
# (Issues, Projects, etc.) use this internally via PlanMyStuff.client.
|
|
10
9
|
class Client
|
|
11
10
|
# @return [Octokit::Client]
|
|
12
11
|
attr_reader :octokit
|
|
13
12
|
|
|
13
|
+
# Returns the Faraday response from the most recent Octokit call. Useful for reading headers (e.g. `ETag`,
|
|
14
|
+
# `X-RateLimit-Remaining`) that aren't included in the parsed response body.
|
|
15
|
+
#
|
|
16
|
+
# @return [Faraday::Response, nil]
|
|
17
|
+
delegate :last_response, to: :octokit
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Activates trace mode: every +rest+ and +graphql+ call is recorded into +traced_requests+ until
|
|
21
|
+
# +exit_trace_mode!+ is called. Useful for debugging which GitHub calls a code path makes.
|
|
22
|
+
#
|
|
23
|
+
# @return [void]
|
|
24
|
+
#
|
|
25
|
+
def trace_mode!
|
|
26
|
+
@trace_mode = true
|
|
27
|
+
@traced_requests = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Disables trace mode and clears any recorded requests.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
#
|
|
34
|
+
def exit_trace_mode!
|
|
35
|
+
@trace_mode = false
|
|
36
|
+
@traced_requests = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def trace_mode?
|
|
41
|
+
@trace_mode == true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<Hash>]
|
|
45
|
+
def traced_requests
|
|
46
|
+
@traced_requests ||= []
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @raise [PlanMyStuff::ConfigurationError] if +import_access_token+ is blank when +importing: true+
|
|
51
|
+
#
|
|
14
52
|
# @return [Client]
|
|
15
|
-
|
|
53
|
+
#
|
|
54
|
+
def initialize(importing: false)
|
|
16
55
|
PlanMyStuff.configuration.validate!
|
|
17
56
|
|
|
18
|
-
|
|
57
|
+
access_token = importing ? PlanMyStuff.configuration.import_access_token : PlanMyStuff.configuration.access_token
|
|
58
|
+
|
|
59
|
+
if importing && access_token.blank?
|
|
60
|
+
raise(PlanMyStuff::ConfigurationError, 'Import access token is required for import client but not configured')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@octokit = Octokit::Client.new(access_token: access_token)
|
|
19
64
|
end
|
|
20
65
|
|
|
21
66
|
# Delegates a REST API call to Octokit, normalizing errors.
|
|
@@ -26,16 +71,10 @@ module PlanMyStuff
|
|
|
26
71
|
#
|
|
27
72
|
# @return [Object] Octokit response
|
|
28
73
|
#
|
|
29
|
-
def rest(method,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
else
|
|
33
|
-
octokit.public_send(method, *, **kwargs, &)
|
|
74
|
+
def rest(method, *args, **kwargs, &block)
|
|
75
|
+
trace!(:rest, method: method, args: args, kwargs: kwargs) do
|
|
76
|
+
execute_rest!(method, *args, **kwargs, &block)
|
|
34
77
|
end
|
|
35
|
-
rescue Octokit::TooManyRequests => e
|
|
36
|
-
raise_rate_limit_error(e)
|
|
37
|
-
rescue Octokit::ClientError, Octokit::ServerError => e
|
|
38
|
-
raise(APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
|
|
39
78
|
end
|
|
40
79
|
|
|
41
80
|
# Executes a GraphQL query against GitHub's /graphql endpoint.
|
|
@@ -46,65 +85,101 @@ module PlanMyStuff
|
|
|
46
85
|
# @return [Hash] parsed response data
|
|
47
86
|
#
|
|
48
87
|
def graphql(query, variables: {})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
response = octokit.post('/graphql', payload.to_json)
|
|
53
|
-
data =
|
|
54
|
-
if response.is_a?(Hash)
|
|
55
|
-
response
|
|
56
|
-
else
|
|
57
|
-
(response.respond_to?(:to_h) ? response.to_h : response)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
data = data.deep_symbolize_keys if data.respond_to?(:deep_symbolize_keys)
|
|
61
|
-
|
|
62
|
-
check_graphql_errors!(data)
|
|
63
|
-
|
|
64
|
-
data[:data]
|
|
65
|
-
rescue Octokit::TooManyRequests => e
|
|
66
|
-
raise_rate_limit_error(e)
|
|
67
|
-
rescue Octokit::ClientError, Octokit::ServerError => e
|
|
68
|
-
raise(APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
|
|
88
|
+
trace!(:graphql, query: query, variables: variables) do
|
|
89
|
+
execute_graphql!(query, variables)
|
|
90
|
+
end
|
|
69
91
|
end
|
|
70
92
|
|
|
71
93
|
# Resolves a repo param to a full "Org/Repo" string.
|
|
72
94
|
#
|
|
73
|
-
# @param repo [Symbol, String, nil] repo key, full string, or nil for default
|
|
95
|
+
# @param repo [Symbol, String, PlanMyStuff::Repo, nil] repo key, full string, Repo instance, or nil for default
|
|
74
96
|
#
|
|
75
97
|
# @return [String] full repo path (e.g. "BrandsInsurance/Element")
|
|
76
98
|
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
def resolve_repo!(repo = nil)
|
|
100
|
+
PlanMyStuff::Repo.resolve!(repo).full_name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# @raise [PlanMyStuff::APIError] if Octokit call fails with client or server error
|
|
106
|
+
#
|
|
107
|
+
# @return [Object]
|
|
108
|
+
#
|
|
109
|
+
def execute_rest!(method, *, **kwargs, &)
|
|
110
|
+
if kwargs.empty?
|
|
111
|
+
octokit.public_send(method, *, &)
|
|
112
|
+
else
|
|
113
|
+
octokit.public_send(method, *, **kwargs, &)
|
|
114
|
+
end
|
|
115
|
+
rescue Octokit::TooManyRequests => e
|
|
116
|
+
raise_rate_limit_error!(e)
|
|
117
|
+
rescue Octokit::ClientError, Octokit::ServerError => e
|
|
118
|
+
raise(PlanMyStuff::APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
|
|
88
119
|
end
|
|
89
120
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
# @raise [PlanMyStuff::APIError] if Octokit call fails with client or server error
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash]
|
|
124
|
+
#
|
|
125
|
+
def execute_graphql!(query, variables)
|
|
126
|
+
payload = { query: query }
|
|
127
|
+
payload[:variables] = variables unless variables.empty?
|
|
128
|
+
|
|
129
|
+
response = octokit.post('/graphql', payload.to_json)
|
|
130
|
+
data =
|
|
131
|
+
if response.is_a?(Hash)
|
|
132
|
+
response
|
|
133
|
+
else
|
|
134
|
+
(response.respond_to?(:to_h) ? response.to_h : response)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
data = data.deep_symbolize_keys if data.respond_to?(:deep_symbolize_keys)
|
|
138
|
+
|
|
139
|
+
check_graphql_errors!(data)
|
|
140
|
+
|
|
141
|
+
data[:data]
|
|
142
|
+
rescue Octokit::TooManyRequests => e
|
|
143
|
+
raise_rate_limit_error!(e)
|
|
144
|
+
rescue Octokit::ClientError, Octokit::ServerError => e
|
|
145
|
+
raise(PlanMyStuff::APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
|
|
146
|
+
end
|
|
94
147
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
148
|
+
# Records the wrapped call into +Client.traced_requests+ when trace mode is on; otherwise just yields.
|
|
149
|
+
#
|
|
150
|
+
# @param kind [Symbol] :rest or :graphql
|
|
151
|
+
# @param details [Hash] call-specific details (method/args/query/variables)
|
|
152
|
+
#
|
|
153
|
+
# @return [Object]
|
|
154
|
+
#
|
|
155
|
+
def trace!(kind, **details)
|
|
156
|
+
return yield unless self.class.trace_mode?
|
|
157
|
+
|
|
158
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
|
+
begin
|
|
160
|
+
result = yield
|
|
161
|
+
record_trace(kind, start, details)
|
|
162
|
+
result
|
|
163
|
+
rescue => e
|
|
164
|
+
record_trace(kind, start, details, error: e)
|
|
165
|
+
raise
|
|
166
|
+
end
|
|
100
167
|
end
|
|
101
|
-
end
|
|
102
168
|
|
|
103
|
-
|
|
169
|
+
# @return [void]
|
|
170
|
+
def record_trace(kind, start, details, error: nil)
|
|
171
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1_000).round(2)
|
|
172
|
+
entry = { kind: kind, duration_ms: duration_ms, status: octokit.last_response&.status, **details }
|
|
173
|
+
if error
|
|
174
|
+
entry[:error] = error.class.name
|
|
175
|
+
entry[:error_message] = error.message
|
|
176
|
+
end
|
|
177
|
+
self.class.traced_requests << entry
|
|
178
|
+
end
|
|
104
179
|
|
|
105
|
-
# @
|
|
180
|
+
# @raise [PlanMyStuff::GraphQLError] if response contains errors
|
|
106
181
|
#
|
|
107
|
-
# @
|
|
182
|
+
# @param data [Hash] parsed GraphQL response
|
|
108
183
|
#
|
|
109
184
|
# @return [void]
|
|
110
185
|
#
|
|
@@ -113,16 +188,16 @@ module PlanMyStuff
|
|
|
113
188
|
return if errors.blank?
|
|
114
189
|
|
|
115
190
|
messages = errors.filter_map { |e| e[:message] }
|
|
116
|
-
raise(GraphQLError.new(messages.join('; '), errors: errors))
|
|
191
|
+
raise(PlanMyStuff::GraphQLError.new(messages.join('; '), errors: errors))
|
|
117
192
|
end
|
|
118
193
|
|
|
119
|
-
# @
|
|
194
|
+
# @raise [PlanMyStuff::RateLimitError] if GitHub has been rate limited
|
|
120
195
|
#
|
|
121
|
-
# @
|
|
196
|
+
# @param exception [Octokit::TooManyRequests]
|
|
122
197
|
#
|
|
123
|
-
def raise_rate_limit_error(exception)
|
|
198
|
+
def raise_rate_limit_error!(exception)
|
|
124
199
|
retry_after = parse_retry_after(exception)
|
|
125
|
-
raise(RateLimitError.new(exception.message, retry_after: retry_after))
|
|
200
|
+
raise(PlanMyStuff::RateLimitError.new(exception.message, retry_after: retry_after))
|
|
126
201
|
end
|
|
127
202
|
|
|
128
203
|
# @param exception [Octokit::TooManyRequests]
|