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,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module PlanMyStuff
|
|
9
|
+
# Dev helper: fetch recent GitHub webhook deliveries via the GitHub API
|
|
10
|
+
# and POST each one to a local endpoint, so a webhook flow can be
|
|
11
|
+
# reproduced against a running server without waiting for GitHub to
|
|
12
|
+
# re-deliver.
|
|
13
|
+
#
|
|
14
|
+
# Intended for use via +plan_my_stuff:webhooks:replay+, not production
|
|
15
|
+
# code paths.
|
|
16
|
+
module WebhookReplayer
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# POST a single webhook delivery to a local endpoint.
|
|
20
|
+
#
|
|
21
|
+
# @param headers [Hash]
|
|
22
|
+
# @param payload [String, Hash, Array]
|
|
23
|
+
# @param endpoint_url [String] full URL including path
|
|
24
|
+
#
|
|
25
|
+
# @return [Net::HTTPResponse]
|
|
26
|
+
#
|
|
27
|
+
def post(headers:, payload:, endpoint_url:)
|
|
28
|
+
uri = URI(endpoint_url)
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = uri.scheme == 'https'
|
|
31
|
+
http.open_timeout = 3_600
|
|
32
|
+
http.read_timeout = 3_600
|
|
33
|
+
|
|
34
|
+
body = payload.is_a?(String) ? JSON.parse(payload).to_json : JSON.generate(payload)
|
|
35
|
+
|
|
36
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
37
|
+
headers.each { |key, value| request[key.to_s] = value.to_s }
|
|
38
|
+
request['Content-Type'] ||= 'application/json'
|
|
39
|
+
request.body = body
|
|
40
|
+
|
|
41
|
+
$stdout.puts("POST #{endpoint_url}")
|
|
42
|
+
$stdout.puts("Headers: #{headers.keys.join(', ')}")
|
|
43
|
+
$stdout.puts('---')
|
|
44
|
+
|
|
45
|
+
response = http.request(request)
|
|
46
|
+
$stdout.puts("HTTP #{response.code} #{response.message}")
|
|
47
|
+
$stdout.puts(response.body) if response.body.present?
|
|
48
|
+
response
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Fetch deliveries for a hook and replay unseen ones.
|
|
52
|
+
#
|
|
53
|
+
# @param endpoint_url [String] local URL to POST replays to
|
|
54
|
+
# @param webhook_url [String] remote config.url used to resolve hook id
|
|
55
|
+
# @param scope [Symbol, String] :org or :repo
|
|
56
|
+
# @param repo [String, nil] required when scope is :repo
|
|
57
|
+
# @param processed_file [String] state file tracking replayed delivery ids
|
|
58
|
+
# @param interactive [Boolean] prompt after each successful delivery
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
#
|
|
62
|
+
def fetch_and_replay(endpoint_url:, webhook_url:, processed_file:, scope: :org, repo: nil, interactive: true)
|
|
63
|
+
scope = scope.to_sym
|
|
64
|
+
hook_id = resolve_hook_id(scope: scope, repo: repo, webhook_url: webhook_url)
|
|
65
|
+
deliveries_path = "#{hooks_base_path(scope: scope, repo: repo)}/#{hook_id}/deliveries"
|
|
66
|
+
$stdout.puts("Using #{scope} hook #{hook_id} -> #{webhook_url}")
|
|
67
|
+
|
|
68
|
+
processed = load_processed(processed_file)
|
|
69
|
+
$stdout.puts("Already processed: #{processed.size}")
|
|
70
|
+
|
|
71
|
+
deliveries = gh_get(deliveries_path, per_page: 50).sort_by { |d| d['delivered_at'].to_s }
|
|
72
|
+
to_process = deliveries.reject { |d| processed.include?(d['id'].to_s) }
|
|
73
|
+
$stdout.puts("Fetched #{deliveries.size} deliveries, #{to_process.size} new")
|
|
74
|
+
if to_process.empty?
|
|
75
|
+
$stdout.puts('Nothing to do.')
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
replay_each(to_process, deliveries_path, endpoint_url, processed_file, interactive: interactive)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Polls one or more webhooks on an interval and auto-replays new
|
|
83
|
+
# deliveries as they arrive. Resolves hook ids once up front, then
|
|
84
|
+
# loops forever until Ctrl-C.
|
|
85
|
+
#
|
|
86
|
+
# @param targets [Array<Hash>] one hash per hook, each with
|
|
87
|
+
# +:scope+ (:org / :repo), +:webhook_url+, and +:repo+ (for :repo scope)
|
|
88
|
+
# @param endpoint_url [String] local URL to POST replays to
|
|
89
|
+
# @param processed_file_for [#call] lambda taking a target hash,
|
|
90
|
+
# returns the state file path for that target
|
|
91
|
+
# @param interval [Integer] seconds between polls (default 15)
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
#
|
|
95
|
+
def listen(targets:, endpoint_url:, processed_file_for:, interval: 30)
|
|
96
|
+
resolved = targets.map { |t| resolve_target(t, processed_file_for) }
|
|
97
|
+
$stdout.puts("Listening on #{resolved.size} hook(s) every #{interval}s. Ctrl-C to stop.")
|
|
98
|
+
resolved.each do |r|
|
|
99
|
+
$stdout.puts(" #{r[:scope]} hook #{r[:hook_id]} -> #{r[:webhook_url]}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
loop do
|
|
103
|
+
poll_all(resolved, endpoint_url)
|
|
104
|
+
$stdout.puts('=============')
|
|
105
|
+
$stdout.puts(' Sleeping...')
|
|
106
|
+
$stdout.puts('=============')
|
|
107
|
+
sleep(interval)
|
|
108
|
+
end
|
|
109
|
+
rescue Interrupt
|
|
110
|
+
$stdout.puts("\nStopped.")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Resolves a listen target's hook id and precomputes its deliveries
|
|
114
|
+
# path + processed-file location.
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash]
|
|
117
|
+
#
|
|
118
|
+
def resolve_target(target, processed_file_for)
|
|
119
|
+
scope = target.fetch(:scope).to_sym
|
|
120
|
+
repo = target[:repo]
|
|
121
|
+
webhook_url = target.fetch(:webhook_url)
|
|
122
|
+
hook_id = resolve_hook_id(scope: scope, repo: repo, webhook_url: webhook_url)
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
scope: scope,
|
|
126
|
+
repo: repo,
|
|
127
|
+
webhook_url: webhook_url,
|
|
128
|
+
hook_id: hook_id,
|
|
129
|
+
deliveries_path: "#{hooks_base_path(scope: scope, repo: repo)}/#{hook_id}/deliveries",
|
|
130
|
+
processed_file: processed_file_for.call(target),
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Collects unprocessed deliveries across every resolved target,
|
|
135
|
+
# merges them into a single list sorted by +delivered_at+, then
|
|
136
|
+
# replays them one-by-one in true chronological order. Transient
|
|
137
|
+
# API errors per target are logged and swallowed so the listen
|
|
138
|
+
# loop survives.
|
|
139
|
+
#
|
|
140
|
+
# @param resolved [Array<Hash>]
|
|
141
|
+
# @param endpoint_url [String]
|
|
142
|
+
#
|
|
143
|
+
# @return [void]
|
|
144
|
+
#
|
|
145
|
+
def poll_all(resolved, endpoint_url)
|
|
146
|
+
pending = []
|
|
147
|
+
resolved.each do |target|
|
|
148
|
+
processed = load_processed(target[:processed_file])
|
|
149
|
+
deliveries = gh_get(target[:deliveries_path], per_page: 50)
|
|
150
|
+
deliveries.each do |summary|
|
|
151
|
+
next if processed.include?(summary['id'].to_s)
|
|
152
|
+
|
|
153
|
+
pending << { target: target, summary: summary }
|
|
154
|
+
end
|
|
155
|
+
rescue Octokit::Error => e
|
|
156
|
+
warn("[#{target[:scope]}] poll failed: #{e.message}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return if pending.empty?
|
|
160
|
+
|
|
161
|
+
pending.sort_by! { |item| item[:summary]['delivered_at'].to_s }
|
|
162
|
+
$stdout.puts("Replaying #{pending.size} delivery(ies) in delivered_at order")
|
|
163
|
+
replay_pending(pending, endpoint_url)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Replays a pre-sorted, multi-target pending list one delivery at
|
|
167
|
+
# a time. Stops on the first non-success so state can be
|
|
168
|
+
# investigated without skipping ahead.
|
|
169
|
+
#
|
|
170
|
+
# @raise [PlanMyStuff::Error] when the endpoint returns a non-success HTTP status
|
|
171
|
+
#
|
|
172
|
+
# @return [void]
|
|
173
|
+
#
|
|
174
|
+
def replay_pending(pending, endpoint_url)
|
|
175
|
+
pending.each.with_index(1) do |item, index|
|
|
176
|
+
target = item[:target]
|
|
177
|
+
summary = item[:summary]
|
|
178
|
+
id = summary['id']
|
|
179
|
+
delivery = gh_get("#{target[:deliveries_path]}/#{id}")
|
|
180
|
+
request_data = delivery.fetch('request')
|
|
181
|
+
github_delivery_id = request_data.dig('headers', 'X-GitHub-Delivery') || id
|
|
182
|
+
|
|
183
|
+
event = "#{summary['event']}.#{summary['action']}"
|
|
184
|
+
$stdout.puts(
|
|
185
|
+
"[#{index}/#{pending.size}] #{target[:scope]} #{id} #{event} " \
|
|
186
|
+
"@ #{summary['delivered_at']} - #{github_delivery_id}",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
response = post(
|
|
190
|
+
headers: request_data.fetch('headers') || {},
|
|
191
|
+
payload: request_data.fetch('payload'),
|
|
192
|
+
endpoint_url: endpoint_url,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
196
|
+
raise(PlanMyStuff::Error, ' ! non-success; stopping so you can investigate')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
mark_processed(target[:processed_file], id)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @raise [ArgumentError] when scope is :repo but repo is nil or empty
|
|
204
|
+
# @raise [ArgumentError] when scope is neither :org nor :repo
|
|
205
|
+
#
|
|
206
|
+
# @return [String]
|
|
207
|
+
#
|
|
208
|
+
def hooks_base_path(scope:, repo:)
|
|
209
|
+
case scope
|
|
210
|
+
when :org
|
|
211
|
+
"/orgs/#{PlanMyStuff.configuration.organization}/hooks"
|
|
212
|
+
when :repo
|
|
213
|
+
raise(ArgumentError, 'repo is required for :repo scope') if repo.nil? || repo.to_s.empty?
|
|
214
|
+
|
|
215
|
+
"/repos/#{repo}/hooks"
|
|
216
|
+
else
|
|
217
|
+
raise(ArgumentError, "Unknown scope #{scope.inspect}; expected :org or :repo")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @raise [PlanMyStuff::Error] when no webhook matches the given URL
|
|
222
|
+
#
|
|
223
|
+
# @return [Integer]
|
|
224
|
+
#
|
|
225
|
+
def resolve_hook_id(scope:, repo:, webhook_url:)
|
|
226
|
+
hooks = gh_get(hooks_base_path(scope: scope, repo: repo), per_page: 100)
|
|
227
|
+
match = hooks.find { |h| h.dig('config', 'url') == webhook_url }
|
|
228
|
+
return match['id'] if match.present?
|
|
229
|
+
|
|
230
|
+
available = hooks.map { |h| " id=#{h['id']} url=#{h.dig('config', 'url')}" }.join("\n")
|
|
231
|
+
raise(PlanMyStuff::Error, "No #{scope} webhook with config.url == #{webhook_url}. Visible:\n#{available}")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Calls +octokit.get+ and returns the parsed JSON body (string-keyed
|
|
235
|
+
# Hash/Array) rather than Sawyer::Resource, so delivery payload and
|
|
236
|
+
# headers are easy to re-serialize.
|
|
237
|
+
#
|
|
238
|
+
# @return [Array, Hash]
|
|
239
|
+
#
|
|
240
|
+
def gh_get(path, **params)
|
|
241
|
+
client = PlanMyStuff.client.octokit
|
|
242
|
+
client.get(path, params)
|
|
243
|
+
JSON.parse(client.last_response.body)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @return [Set<String>]
|
|
247
|
+
def load_processed(path)
|
|
248
|
+
return Set.new unless File.exist?(path)
|
|
249
|
+
|
|
250
|
+
File.readlines(path, chomp: true).reject(&:empty?).to_set
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @return [void]
|
|
254
|
+
def mark_processed(path, id)
|
|
255
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
256
|
+
File.open(path, 'a') { |f| f.puts(id) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# @raise [PlanMyStuff::Error] when the endpoint returns a non-success HTTP status
|
|
260
|
+
#
|
|
261
|
+
# @return [void]
|
|
262
|
+
#
|
|
263
|
+
def replay_each(to_process, deliveries_path, endpoint_url, processed_file, interactive:)
|
|
264
|
+
to_process.each.with_index(1) do |summary, index|
|
|
265
|
+
id = summary['id']
|
|
266
|
+
delivery = gh_get("#{deliveries_path}/#{id}")
|
|
267
|
+
request_data = delivery.fetch('request')
|
|
268
|
+
github_delivery_id = request_data.dig('headers', 'X-GitHub-Delivery') || id
|
|
269
|
+
|
|
270
|
+
event = "#{summary['event']}.#{summary['action']}"
|
|
271
|
+
$stdout.puts("[#{index}/#{to_process.size}] #{id} #{event} - #{github_delivery_id}")
|
|
272
|
+
|
|
273
|
+
response = post(
|
|
274
|
+
headers: request_data.fetch('headers') || {},
|
|
275
|
+
payload: request_data.fetch('payload'),
|
|
276
|
+
endpoint_url: endpoint_url,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
280
|
+
raise(PlanMyStuff::Error, ' ! non-success; stopping so you can investigate')
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
mark_processed(processed_file, id)
|
|
284
|
+
next unless interactive
|
|
285
|
+
|
|
286
|
+
$stdout.puts("Processed #{github_delivery_id} - Continue? (y/n)")
|
|
287
|
+
answer = $stdin.gets
|
|
288
|
+
break unless answer&.downcase&.start_with?('y')
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
|
|
5
|
+
require 'date'
|
|
6
|
+
|
|
3
7
|
require 'active_support/core_ext/array/wrap'
|
|
4
8
|
require 'active_support/core_ext/object/blank'
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
14
28
|
require_relative 'plan_my_stuff/errors'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
require_relative 'plan_my_stuff/
|
|
19
|
-
require_relative 'plan_my_stuff/metadata_parser'
|
|
20
|
-
require_relative 'plan_my_stuff/project'
|
|
21
|
-
require_relative 'plan_my_stuff/project_item'
|
|
22
|
-
require_relative 'plan_my_stuff/user_resolver'
|
|
23
|
-
require_relative 'plan_my_stuff/verifier'
|
|
24
|
-
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)
|
|
25
33
|
|
|
26
34
|
module PlanMyStuff
|
|
27
35
|
class << self
|
|
28
36
|
# @return [PlanMyStuff::Configuration]
|
|
29
37
|
def configuration
|
|
30
|
-
@configuration ||= Configuration.new
|
|
38
|
+
@configuration ||= PlanMyStuff::Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [ActiveSupport::Deprecation]
|
|
42
|
+
def deprecator
|
|
43
|
+
@deprecator ||= ActiveSupport::Deprecation.new('1.0', 'PlanMyStuff')
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
# @return [PlanMyStuff::Configuration]
|
|
@@ -37,7 +50,12 @@ module PlanMyStuff
|
|
|
37
50
|
|
|
38
51
|
# @return [PlanMyStuff::Client]
|
|
39
52
|
def client
|
|
40
|
-
@client ||= Client.new
|
|
53
|
+
@client ||= PlanMyStuff::Client.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [PlanMyStuff::Client]
|
|
57
|
+
def import_client
|
|
58
|
+
@import_client ||= PlanMyStuff::Client.new(importing: true)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
# Returns the appropriate HTTP 422 status symbol for the current Rails version.
|
|
@@ -53,13 +71,30 @@ module PlanMyStuff
|
|
|
53
71
|
end
|
|
54
72
|
end
|
|
55
73
|
|
|
74
|
+
# Formats a time-ish value as an ISO 8601 string. +Time+ and
|
|
75
|
+
# +DateTime+ are normalized to UTC; +Date+ is serialized as-is.
|
|
76
|
+
#
|
|
77
|
+
# @param value [Time, DateTime, Date, String, nil]
|
|
78
|
+
#
|
|
79
|
+
# @return [String, nil]
|
|
80
|
+
#
|
|
81
|
+
def format_time(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
return value if value.is_a?(String)
|
|
84
|
+
return value.iso8601 if value.is_a?(Date) && !value.is_a?(DateTime)
|
|
85
|
+
|
|
86
|
+
value.utc.iso8601
|
|
87
|
+
end
|
|
88
|
+
|
|
56
89
|
# Resets the memoized client and configuration. Useful for testing.
|
|
57
90
|
#
|
|
58
91
|
# @return [void]
|
|
59
92
|
#
|
|
60
93
|
def reset!
|
|
61
94
|
exit_test_mode! if defined?(@_test_mode) && @_test_mode
|
|
95
|
+
PlanMyStuff::Client.exit_trace_mode!
|
|
62
96
|
@client = nil
|
|
97
|
+
@import_client = nil
|
|
63
98
|
@configuration = nil
|
|
64
99
|
end
|
|
65
100
|
end
|