plan_my_stuff 0.2.0 → 0.4.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 +10 -0
- data/README.md +569 -38
- data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
- data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
- data/app/controllers/plan_my_stuff/projects_controller.rb +65 -1
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
- data/app/jobs/plan_my_stuff/application_job.rb +9 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
- data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +17 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +11 -4
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +38 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +138 -5
- data/lib/plan_my_stuff/application_record.rb +144 -0
- data/lib/plan_my_stuff/approval.rb +80 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +14 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
- data/lib/plan_my_stuff/base_metadata.rb +0 -11
- data/lib/plan_my_stuff/base_project.rb +661 -0
- data/lib/plan_my_stuff/base_project_item.rb +562 -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 +7 -0
- data/lib/plan_my_stuff/comment.rb +174 -54
- data/lib/plan_my_stuff/configuration.rb +254 -8
- data/lib/plan_my_stuff/custom_fields.rb +31 -17
- data/lib/plan_my_stuff/engine.rb +0 -4
- data/lib/plan_my_stuff/errors.rb +49 -0
- data/lib/plan_my_stuff/graphql/queries.rb +392 -0
- data/lib/plan_my_stuff/issue.rb +1477 -174
- data/lib/plan_my_stuff/issue_metadata.rb +122 -0
- data/lib/plan_my_stuff/label.rb +82 -11
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/notifications.rb +142 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +44 -0
- data/lib/plan_my_stuff/pipeline.rb +293 -0
- data/lib/plan_my_stuff/project.rb +62 -468
- data/lib/plan_my_stuff/project_item.rb +3 -417
- 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 +16 -0
- data/lib/plan_my_stuff/test_helpers.rb +260 -15
- data/lib/plan_my_stuff/testing_project.rb +291 -0
- data/lib/plan_my_stuff/testing_project_item.rb +184 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +8 -3
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
- data/lib/plan_my_stuff.rb +16 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +54 -2
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
# @return [void]
|
|
171
|
+
#
|
|
172
|
+
def replay_pending(pending, endpoint_url)
|
|
173
|
+
pending.each.with_index(1) do |item, index|
|
|
174
|
+
target = item[:target]
|
|
175
|
+
summary = item[:summary]
|
|
176
|
+
id = summary['id']
|
|
177
|
+
delivery = gh_get("#{target[:deliveries_path]}/#{id}")
|
|
178
|
+
request_data = delivery.fetch('request')
|
|
179
|
+
github_delivery_id = request_data.dig('headers', 'X-GitHub-Delivery') || id
|
|
180
|
+
|
|
181
|
+
event = "#{summary['event']}.#{summary['action']}"
|
|
182
|
+
$stdout.puts(
|
|
183
|
+
"[#{index}/#{pending.size}] #{target[:scope]} #{id} #{event} " \
|
|
184
|
+
"@ #{summary['delivered_at']} - #{github_delivery_id}",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
response = post(
|
|
188
|
+
headers: request_data.fetch('headers') || {},
|
|
189
|
+
payload: request_data.fetch('payload'),
|
|
190
|
+
endpoint_url: endpoint_url,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
194
|
+
raise(' ! non-success; stopping so you can investigate')
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
mark_processed(target[:processed_file], id)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [String]
|
|
202
|
+
def hooks_base_path(scope:, repo:)
|
|
203
|
+
case scope
|
|
204
|
+
when :org
|
|
205
|
+
"/orgs/#{PlanMyStuff.configuration.organization}/hooks"
|
|
206
|
+
when :repo
|
|
207
|
+
raise(ArgumentError, 'repo is required for :repo scope') if repo.nil? || repo.to_s.empty?
|
|
208
|
+
|
|
209
|
+
"/repos/#{repo}/hooks"
|
|
210
|
+
else
|
|
211
|
+
raise(ArgumentError, "Unknown scope #{scope.inspect}; expected :org or :repo")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @return [Integer]
|
|
216
|
+
def resolve_hook_id(scope:, repo:, webhook_url:)
|
|
217
|
+
hooks = gh_get(hooks_base_path(scope: scope, repo: repo), per_page: 100)
|
|
218
|
+
match = hooks.find { |h| h.dig('config', 'url') == webhook_url }
|
|
219
|
+
return match['id'] if match.present?
|
|
220
|
+
|
|
221
|
+
available = hooks.map { |h| " id=#{h['id']} url=#{h.dig('config', 'url')}" }.join("\n")
|
|
222
|
+
raise(PlanMyStuff::Error, "No #{scope} webhook with config.url == #{webhook_url}. Visible:\n#{available}")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Calls +octokit.get+ and returns the parsed JSON body (string-keyed
|
|
226
|
+
# Hash/Array) rather than Sawyer::Resource, so delivery payload and
|
|
227
|
+
# headers are easy to re-serialize.
|
|
228
|
+
#
|
|
229
|
+
# @return [Array, Hash]
|
|
230
|
+
#
|
|
231
|
+
def gh_get(path, **params)
|
|
232
|
+
client = PlanMyStuff.client.octokit
|
|
233
|
+
client.get(path, params)
|
|
234
|
+
JSON.parse(client.last_response.body)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# @return [Set<String>]
|
|
238
|
+
def load_processed(path)
|
|
239
|
+
return Set.new unless File.exist?(path)
|
|
240
|
+
|
|
241
|
+
File.readlines(path, chomp: true).reject(&:empty?).to_set
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @return [void]
|
|
245
|
+
def mark_processed(path, id)
|
|
246
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
247
|
+
File.open(path, 'a') { |f| f.puts(id) }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @return [void]
|
|
251
|
+
def replay_each(to_process, deliveries_path, endpoint_url, processed_file, interactive:)
|
|
252
|
+
to_process.each.with_index(1) do |summary, index|
|
|
253
|
+
id = summary['id']
|
|
254
|
+
delivery = gh_get("#{deliveries_path}/#{id}")
|
|
255
|
+
request_data = delivery.fetch('request')
|
|
256
|
+
github_delivery_id = request_data.dig('headers', 'X-GitHub-Delivery') || id
|
|
257
|
+
|
|
258
|
+
event = "#{summary['event']}.#{summary['action']}"
|
|
259
|
+
$stdout.puts("[#{index}/#{to_process.size}] #{id} #{event} - #{github_delivery_id}")
|
|
260
|
+
|
|
261
|
+
response = post(
|
|
262
|
+
headers: request_data.fetch('headers') || {},
|
|
263
|
+
payload: request_data.fetch('payload'),
|
|
264
|
+
endpoint_url: endpoint_url,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
268
|
+
raise(' ! non-success; stopping so you can investigate')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
mark_processed(processed_file, id)
|
|
272
|
+
next unless interactive
|
|
273
|
+
|
|
274
|
+
$stdout.puts("Processed #{github_delivery_id} - Continue? (y/n)")
|
|
275
|
+
answer = $stdin.gets
|
|
276
|
+
break unless answer&.downcase&.start_with?('y')
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -4,7 +4,13 @@ require 'active_support/core_ext/array/wrap'
|
|
|
4
4
|
require 'active_support/core_ext/object/blank'
|
|
5
5
|
|
|
6
6
|
require_relative 'plan_my_stuff/application_record'
|
|
7
|
+
require_relative 'plan_my_stuff/approval'
|
|
8
|
+
require_relative 'plan_my_stuff/archive'
|
|
7
9
|
require_relative 'plan_my_stuff/base_metadata'
|
|
10
|
+
require_relative 'plan_my_stuff/base_project'
|
|
11
|
+
require_relative 'plan_my_stuff/base_project_item'
|
|
12
|
+
require_relative 'plan_my_stuff/base_project_metadata'
|
|
13
|
+
require_relative 'plan_my_stuff/cache'
|
|
8
14
|
require_relative 'plan_my_stuff/client'
|
|
9
15
|
require_relative 'plan_my_stuff/comment'
|
|
10
16
|
require_relative 'plan_my_stuff/comment_metadata'
|
|
@@ -12,14 +18,24 @@ require_relative 'plan_my_stuff/configuration'
|
|
|
12
18
|
require_relative 'plan_my_stuff/custom_fields'
|
|
13
19
|
require_relative 'plan_my_stuff/engine' if defined?(Rails)
|
|
14
20
|
require_relative 'plan_my_stuff/errors'
|
|
21
|
+
require_relative 'plan_my_stuff/graphql/queries'
|
|
15
22
|
require_relative 'plan_my_stuff/issue'
|
|
16
23
|
require_relative 'plan_my_stuff/issue_metadata'
|
|
17
24
|
require_relative 'plan_my_stuff/label'
|
|
25
|
+
require_relative 'plan_my_stuff/link'
|
|
18
26
|
require_relative 'plan_my_stuff/markdown'
|
|
19
27
|
require_relative 'plan_my_stuff/metadata_parser'
|
|
28
|
+
require_relative 'plan_my_stuff/notifications'
|
|
29
|
+
require_relative 'plan_my_stuff/pipeline'
|
|
20
30
|
require_relative 'plan_my_stuff/project'
|
|
21
31
|
require_relative 'plan_my_stuff/project_item'
|
|
32
|
+
require_relative 'plan_my_stuff/project_item_metadata'
|
|
33
|
+
require_relative 'plan_my_stuff/project_metadata'
|
|
34
|
+
require_relative 'plan_my_stuff/reminders'
|
|
22
35
|
require_relative 'plan_my_stuff/repo'
|
|
36
|
+
require_relative 'plan_my_stuff/testing_project'
|
|
37
|
+
require_relative 'plan_my_stuff/testing_project_item'
|
|
38
|
+
require_relative 'plan_my_stuff/testing_project_metadata'
|
|
23
39
|
require_relative 'plan_my_stuff/user_resolver'
|
|
24
40
|
require_relative 'plan_my_stuff/verifier'
|
|
25
41
|
require_relative 'plan_my_stuff/version'
|
|
@@ -1,6 +1,169 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
namespace :plan_my_stuff do
|
|
4
|
+
namespace :webhooks do
|
|
5
|
+
desc 'Create an organization webhook (URL=... [EVENTS=ev1,ev2])'
|
|
6
|
+
task create_org: :environment do
|
|
7
|
+
url = ENV.fetch('URL') { raise('URL env var is required') }
|
|
8
|
+
events = %w[projects_v2 projects_v2_item projects_v2_status_update]
|
|
9
|
+
config = PlanMyStuff.configuration
|
|
10
|
+
raise('PlanMyStuff.configuration.webhook_secret is blank') if config.webhook_secret.blank?
|
|
11
|
+
|
|
12
|
+
hook = PlanMyStuff.client.rest(
|
|
13
|
+
:create_org_hook,
|
|
14
|
+
config.organization,
|
|
15
|
+
{ url: url, content_type: 'json', secret: config.webhook_secret, insecure_ssl: '0' },
|
|
16
|
+
{ events: events, active: true },
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
puts("Created org hook #{hook.id} on #{config.organization} -> #{url}")
|
|
20
|
+
puts("Events: #{events.join(', ')}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc 'Create a repo webhook (REPO=owner/name URL=... [EVENTS=ev1,ev2])'
|
|
24
|
+
task create_repo: :environment do
|
|
25
|
+
url = ENV.fetch('URL') { raise('URL env var is required') }
|
|
26
|
+
repo = ENV.fetch('REPO') do
|
|
27
|
+
PlanMyStuff.client.resolve_repo ||
|
|
28
|
+
raise('REPO env var is required or configured (e.g. BrandsInsurance/PlanMyStuff)')
|
|
29
|
+
end
|
|
30
|
+
events = %w[pull_request issues]
|
|
31
|
+
config = PlanMyStuff.configuration
|
|
32
|
+
raise('PlanMyStuff.configuration.webhook_secret is blank') if config.webhook_secret.blank?
|
|
33
|
+
|
|
34
|
+
hook = PlanMyStuff.client.rest(
|
|
35
|
+
:create_hook,
|
|
36
|
+
repo,
|
|
37
|
+
'web',
|
|
38
|
+
{ url: url, content_type: 'json', secret: config.webhook_secret, insecure_ssl: '0' },
|
|
39
|
+
{ events: events, active: true },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
puts("Created repo hook #{hook.id} on #{repo} -> #{url}")
|
|
43
|
+
puts("Events: #{events.join(', ')}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc 'Replay recent deliveries to a local endpoint (ENDPOINT_URL=... WEBHOOK_URL=... ' \
|
|
47
|
+
'[SCOPE=org|repo] [REPO=owner/name] [PROCESSED_FILE=...] [NON_INTERACTIVE=1])'
|
|
48
|
+
task replay: :environment do
|
|
49
|
+
require 'plan_my_stuff/webhook_replayer'
|
|
50
|
+
|
|
51
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
52
|
+
webhook_url = ENV.fetch('WEBHOOK_URL') { raise('WEBHOOK_URL env var is required') }
|
|
53
|
+
scope = ENV.fetch('SCOPE', 'org').to_sym
|
|
54
|
+
repo = ENV.fetch('REPO', nil)
|
|
55
|
+
processed_file =
|
|
56
|
+
ENV.fetch('PROCESSED_FILE') do
|
|
57
|
+
Rails.root.join('tmp', 'plan_my_stuff', "webhook_replay_#{scope}_processed.txt").to_s
|
|
58
|
+
end
|
|
59
|
+
interactive = ENV['NON_INTERACTIVE'].to_s.strip.empty?
|
|
60
|
+
|
|
61
|
+
PlanMyStuff::WebhookReplayer.fetch_and_replay(
|
|
62
|
+
endpoint_url: endpoint_url,
|
|
63
|
+
webhook_url: webhook_url,
|
|
64
|
+
scope: scope,
|
|
65
|
+
repo: repo,
|
|
66
|
+
processed_file: processed_file,
|
|
67
|
+
interactive: interactive,
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc 'Continuously poll org + repo hooks and auto-replay new deliveries ' \
|
|
72
|
+
'(ENDPOINT_URL=... [ORG_WEBHOOK_URL=...] [REPO_WEBHOOK_URL=... REPO=owner/name] [INTERVAL=15])'
|
|
73
|
+
task listen: :environment do
|
|
74
|
+
require 'plan_my_stuff/webhook_replayer'
|
|
75
|
+
|
|
76
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
77
|
+
org_webhook_url = ENV.fetch('ORG_WEBHOOK_URL', nil)
|
|
78
|
+
repo_webhook_url = ENV.fetch('REPO_WEBHOOK_URL', nil)
|
|
79
|
+
repo = ENV.fetch('REPO', nil)
|
|
80
|
+
interval = ENV.fetch('INTERVAL', '30').to_i
|
|
81
|
+
|
|
82
|
+
targets = []
|
|
83
|
+
targets << { scope: :org, webhook_url: org_webhook_url } if org_webhook_url.present?
|
|
84
|
+
if repo_webhook_url.present?
|
|
85
|
+
raise('REPO env var required when REPO_WEBHOOK_URL is set') if repo.blank?
|
|
86
|
+
|
|
87
|
+
targets << { scope: :repo, webhook_url: repo_webhook_url, repo: repo }
|
|
88
|
+
end
|
|
89
|
+
raise('Set at least one of ORG_WEBHOOK_URL or REPO_WEBHOOK_URL') if targets.empty?
|
|
90
|
+
|
|
91
|
+
processed_file_for = -> (target) {
|
|
92
|
+
Rails.root.join('tmp', 'plan_my_stuff', "webhook_listen_#{target[:scope]}_processed.txt").to_s
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
PlanMyStuff::WebhookReplayer.listen(
|
|
96
|
+
targets: targets,
|
|
97
|
+
endpoint_url: endpoint_url,
|
|
98
|
+
processed_file_for: processed_file_for,
|
|
99
|
+
interval: interval,
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc 'Simulate an AWS SNS deployment webhook to a local endpoint ' \
|
|
104
|
+
'(ENDPOINT_URL=... [EVENT=SERVICE_DEPLOYMENT_COMPLETED])'
|
|
105
|
+
task simulate_aws: :environment do
|
|
106
|
+
require 'plan_my_stuff/aws_sns_simulator'
|
|
107
|
+
|
|
108
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
109
|
+
event_name = ENV.fetch('EVENT', PlanMyStuff::AwsSnsSimulator::DEFAULT_EVENT)
|
|
110
|
+
|
|
111
|
+
PlanMyStuff::AwsSnsSimulator.post(
|
|
112
|
+
endpoint_url: endpoint_url,
|
|
113
|
+
event_name: event_name,
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
namespace :reminders do
|
|
119
|
+
desc 'Enqueue a RemindersSweepJob per configured repo ' \
|
|
120
|
+
'([REPO=<key>] to target a single repo)'
|
|
121
|
+
task sweep: :environment do
|
|
122
|
+
config = PlanMyStuff.configuration
|
|
123
|
+
repo_keys =
|
|
124
|
+
if ENV['REPO'].present?
|
|
125
|
+
[ENV['REPO'].to_sym]
|
|
126
|
+
else
|
|
127
|
+
config.repos.keys
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise('No repos configured (set config.repos or pass REPO=<key>)') if repo_keys.empty?
|
|
131
|
+
|
|
132
|
+
repo_keys.each do |key|
|
|
133
|
+
PlanMyStuff::RemindersSweepJob.requeue(key)
|
|
134
|
+
puts("Scheduled RemindersSweepJob for #{key.inspect} " \
|
|
135
|
+
"(#{config.repos[key] || '<unknown>'}) at #{PlanMyStuff::RemindersSweepJob.next_run.iso8601}")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
namespace :testing do
|
|
141
|
+
desc 'Create a TestingProject template in the configured organization. ' \
|
|
142
|
+
'Bootstraps all testing custom fields. Set the returned project number as ' \
|
|
143
|
+
'config.testing_template_project_number, then configure its board view ' \
|
|
144
|
+
'and "Column by: Test Status" manually in the GitHub UI. ' \
|
|
145
|
+
'Options: TITLE="My Template" (default: "PMS Testing Template")'
|
|
146
|
+
task create_template: :environment do
|
|
147
|
+
title = ENV.fetch('TITLE', '[TEMPLATE] Testing Feature')
|
|
148
|
+
|
|
149
|
+
puts("Creating testing project template '#{title}' in " \
|
|
150
|
+
"#{PlanMyStuff.configuration.organization}...")
|
|
151
|
+
|
|
152
|
+
project = PlanMyStuff::TestingProject.create!(title: title)
|
|
153
|
+
|
|
154
|
+
puts
|
|
155
|
+
puts("\e[32mTemplate created successfully.\e[0m")
|
|
156
|
+
puts(" Title: #{project.title}")
|
|
157
|
+
puts(" Number: #{project.number}")
|
|
158
|
+
puts(" URL: #{project.url}")
|
|
159
|
+
puts
|
|
160
|
+
puts('Next steps:')
|
|
161
|
+
puts(' 1. Open the project in GitHub and set the board view to "Column by: Test Status"')
|
|
162
|
+
puts(' 2. Add to your PlanMyStuff initializer:')
|
|
163
|
+
puts(" config.testing_template_project_number = #{project.number}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
4
167
|
desc 'Verify PlanMyStuff configuration: token, org, repos, and project access'
|
|
5
168
|
task verify: :environment do
|
|
6
169
|
require 'plan_my_stuff/verifier'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -56,28 +56,64 @@ files:
|
|
|
56
56
|
- README.md
|
|
57
57
|
- app/controllers/plan_my_stuff/application_controller.rb
|
|
58
58
|
- app/controllers/plan_my_stuff/comments_controller.rb
|
|
59
|
+
- app/controllers/plan_my_stuff/issues/approvals_controller.rb
|
|
60
|
+
- app/controllers/plan_my_stuff/issues/closures_controller.rb
|
|
61
|
+
- app/controllers/plan_my_stuff/issues/links_controller.rb
|
|
62
|
+
- app/controllers/plan_my_stuff/issues/takes_controller.rb
|
|
63
|
+
- app/controllers/plan_my_stuff/issues/viewers_controller.rb
|
|
64
|
+
- app/controllers/plan_my_stuff/issues/waitings_controller.rb
|
|
59
65
|
- app/controllers/plan_my_stuff/issues_controller.rb
|
|
60
66
|
- app/controllers/plan_my_stuff/labels_controller.rb
|
|
67
|
+
- app/controllers/plan_my_stuff/project_items/assignments_controller.rb
|
|
68
|
+
- app/controllers/plan_my_stuff/project_items/statuses_controller.rb
|
|
61
69
|
- app/controllers/plan_my_stuff/project_items_controller.rb
|
|
62
70
|
- app/controllers/plan_my_stuff/projects_controller.rb
|
|
71
|
+
- app/controllers/plan_my_stuff/testing_project_items/results_controller.rb
|
|
72
|
+
- app/controllers/plan_my_stuff/testing_project_items_controller.rb
|
|
73
|
+
- app/controllers/plan_my_stuff/testing_projects_controller.rb
|
|
74
|
+
- app/controllers/plan_my_stuff/webhooks/aws_controller.rb
|
|
75
|
+
- app/controllers/plan_my_stuff/webhooks/github_controller.rb
|
|
76
|
+
- app/jobs/plan_my_stuff/application_job.rb
|
|
77
|
+
- app/jobs/plan_my_stuff/reminders_sweep_job.rb
|
|
63
78
|
- app/views/plan_my_stuff/comments/edit.html.erb
|
|
64
79
|
- app/views/plan_my_stuff/comments/partials/_form.html.erb
|
|
65
80
|
- app/views/plan_my_stuff/issues/edit.html.erb
|
|
66
81
|
- app/views/plan_my_stuff/issues/index.html.erb
|
|
67
82
|
- app/views/plan_my_stuff/issues/new.html.erb
|
|
83
|
+
- app/views/plan_my_stuff/issues/partials/_approvals.html.erb
|
|
68
84
|
- app/views/plan_my_stuff/issues/partials/_form.html.erb
|
|
69
85
|
- app/views/plan_my_stuff/issues/partials/_labels.html.erb
|
|
86
|
+
- app/views/plan_my_stuff/issues/partials/_links.html.erb
|
|
70
87
|
- app/views/plan_my_stuff/issues/partials/_viewers.html.erb
|
|
71
88
|
- app/views/plan_my_stuff/issues/show.html.erb
|
|
89
|
+
- app/views/plan_my_stuff/projects/edit.html.erb
|
|
72
90
|
- app/views/plan_my_stuff/projects/index.html.erb
|
|
91
|
+
- app/views/plan_my_stuff/projects/new.html.erb
|
|
92
|
+
- app/views/plan_my_stuff/projects/partials/_form.html.erb
|
|
73
93
|
- app/views/plan_my_stuff/projects/show.html.erb
|
|
94
|
+
- app/views/plan_my_stuff/testing_project_items/new.html.erb
|
|
95
|
+
- app/views/plan_my_stuff/testing_project_items/results/new.html.erb
|
|
96
|
+
- app/views/plan_my_stuff/testing_projects/edit.html.erb
|
|
97
|
+
- app/views/plan_my_stuff/testing_projects/new.html.erb
|
|
98
|
+
- app/views/plan_my_stuff/testing_projects/partials/_form.html.erb
|
|
99
|
+
- app/views/plan_my_stuff/testing_projects/partials/_item.html.erb
|
|
100
|
+
- app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb
|
|
101
|
+
- app/views/plan_my_stuff/testing_projects/show.html.erb
|
|
74
102
|
- config/routes.rb
|
|
75
103
|
- lib/generators/plan_my_stuff/install/install_generator.rb
|
|
76
104
|
- lib/generators/plan_my_stuff/install/templates/initializer.rb
|
|
77
105
|
- lib/generators/plan_my_stuff/views/views_generator.rb
|
|
78
106
|
- lib/plan_my_stuff.rb
|
|
79
107
|
- lib/plan_my_stuff/application_record.rb
|
|
108
|
+
- lib/plan_my_stuff/approval.rb
|
|
109
|
+
- lib/plan_my_stuff/archive.rb
|
|
110
|
+
- lib/plan_my_stuff/archive/sweep.rb
|
|
111
|
+
- lib/plan_my_stuff/aws_sns_simulator.rb
|
|
80
112
|
- lib/plan_my_stuff/base_metadata.rb
|
|
113
|
+
- lib/plan_my_stuff/base_project.rb
|
|
114
|
+
- lib/plan_my_stuff/base_project_item.rb
|
|
115
|
+
- lib/plan_my_stuff/base_project_metadata.rb
|
|
116
|
+
- lib/plan_my_stuff/cache.rb
|
|
81
117
|
- lib/plan_my_stuff/client.rb
|
|
82
118
|
- lib/plan_my_stuff/comment.rb
|
|
83
119
|
- lib/plan_my_stuff/comment_metadata.rb
|
|
@@ -85,18 +121,34 @@ files:
|
|
|
85
121
|
- lib/plan_my_stuff/custom_fields.rb
|
|
86
122
|
- lib/plan_my_stuff/engine.rb
|
|
87
123
|
- lib/plan_my_stuff/errors.rb
|
|
124
|
+
- lib/plan_my_stuff/graphql/queries.rb
|
|
88
125
|
- lib/plan_my_stuff/issue.rb
|
|
89
126
|
- lib/plan_my_stuff/issue_metadata.rb
|
|
90
127
|
- lib/plan_my_stuff/label.rb
|
|
128
|
+
- lib/plan_my_stuff/link.rb
|
|
91
129
|
- lib/plan_my_stuff/markdown.rb
|
|
92
130
|
- lib/plan_my_stuff/metadata_parser.rb
|
|
131
|
+
- lib/plan_my_stuff/notifications.rb
|
|
132
|
+
- lib/plan_my_stuff/pipeline.rb
|
|
133
|
+
- lib/plan_my_stuff/pipeline/issue_linker.rb
|
|
134
|
+
- lib/plan_my_stuff/pipeline/status.rb
|
|
93
135
|
- lib/plan_my_stuff/project.rb
|
|
94
136
|
- lib/plan_my_stuff/project_item.rb
|
|
137
|
+
- lib/plan_my_stuff/project_item_metadata.rb
|
|
138
|
+
- lib/plan_my_stuff/project_metadata.rb
|
|
139
|
+
- lib/plan_my_stuff/reminders.rb
|
|
140
|
+
- lib/plan_my_stuff/reminders/closer.rb
|
|
141
|
+
- lib/plan_my_stuff/reminders/fire.rb
|
|
142
|
+
- lib/plan_my_stuff/reminders/sweep.rb
|
|
95
143
|
- lib/plan_my_stuff/repo.rb
|
|
96
144
|
- lib/plan_my_stuff/test_helpers.rb
|
|
145
|
+
- lib/plan_my_stuff/testing_project.rb
|
|
146
|
+
- lib/plan_my_stuff/testing_project_item.rb
|
|
147
|
+
- lib/plan_my_stuff/testing_project_metadata.rb
|
|
97
148
|
- lib/plan_my_stuff/user_resolver.rb
|
|
98
149
|
- lib/plan_my_stuff/verifier.rb
|
|
99
150
|
- lib/plan_my_stuff/version.rb
|
|
151
|
+
- lib/plan_my_stuff/webhook_replayer.rb
|
|
100
152
|
- lib/tasks/plan_my_stuff.rake
|
|
101
153
|
homepage: https://github.com/brandsinsurance/PlanMyStuff/
|
|
102
154
|
licenses:
|