plan_my_stuff 0.6.0 → 0.8.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 +41 -1
- data/README.md +100 -103
- data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
- data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
- data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
- data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
- data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
- data/app/jobs/plan_my_stuff/application_job.rb +2 -3
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
- data/app/views/plan_my_stuff/partials/_flash.html.erb +4 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
- data/config/routes.rb +2 -2
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +56 -3
- data/lib/plan_my_stuff/approval.rb +12 -4
- data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
- data/lib/plan_my_stuff/base_metadata.rb +4 -15
- data/lib/plan_my_stuff/base_project.rb +68 -55
- data/lib/plan_my_stuff/base_project_item.rb +61 -57
- data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
- data/lib/plan_my_stuff/client.rb +136 -48
- data/lib/plan_my_stuff/comment.rb +57 -57
- data/lib/plan_my_stuff/comment_metadata.rb +1 -1
- data/lib/plan_my_stuff/configuration.rb +95 -82
- data/lib/plan_my_stuff/errors.rb +10 -10
- data/lib/plan_my_stuff/graphql/queries.rb +1 -1
- data/lib/plan_my_stuff/issue.rb +501 -322
- data/lib/plan_my_stuff/issue_metadata.rb +10 -10
- data/lib/plan_my_stuff/label.rb +32 -16
- data/lib/plan_my_stuff/link.rb +15 -15
- data/lib/plan_my_stuff/markdown.rb +12 -6
- data/lib/plan_my_stuff/metadata_parser.rb +3 -1
- data/lib/plan_my_stuff/notifications.rb +1 -1
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
- data/lib/plan_my_stuff/pipeline.rb +61 -83
- data/lib/plan_my_stuff/project.rb +4 -4
- data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
- data/lib/plan_my_stuff/project_metadata.rb +1 -1
- data/lib/plan_my_stuff/reminders/closer.rb +1 -1
- data/lib/plan_my_stuff/reminders/fire.rb +3 -3
- data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
- data/lib/plan_my_stuff/repo.rb +12 -6
- data/lib/plan_my_stuff/test_helpers.rb +11 -11
- data/lib/plan_my_stuff/testing_project.rb +12 -11
- data/lib/plan_my_stuff/testing_project_item.rb +11 -9
- data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
- data/lib/plan_my_stuff.rb +26 -2
- data/lib/tasks/plan_my_stuff.rake +33 -20
- metadata +3 -2
|
@@ -100,19 +100,19 @@ module PlanMyStuff
|
|
|
100
100
|
def pending_approvers
|
|
101
101
|
@issue.pending_approvals.filter_map do |approval|
|
|
102
102
|
PlanMyStuff::UserResolver.resolve(approval.user_id)
|
|
103
|
-
rescue
|
|
103
|
+
rescue ActiveRecord::RecordNotFound
|
|
104
104
|
next
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
# @return [Array<Integer>]
|
|
109
109
|
def effective_reminder_days
|
|
110
|
-
@issue.metadata.reminder_days.presence || PlanMyStuff.configuration.reminder_days
|
|
110
|
+
(@issue.metadata.reminder_days.presence || PlanMyStuff.configuration.reminder_days).sort
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# Time of the next milestone past +now+, or +nil+ when the last
|
|
114
114
|
# milestone has passed. Returned as a native +Time+ (not ISO
|
|
115
|
-
# string) so +IssueMetadata#to_h+'s +format_time+ serializes it
|
|
115
|
+
# string) so +IssueMetadata#to_h+'s +PlanMyStuff.format_time+ serializes it
|
|
116
116
|
# cleanly.
|
|
117
117
|
#
|
|
118
118
|
# @return [Time, nil]
|
|
@@ -23,10 +23,10 @@ module PlanMyStuff
|
|
|
23
23
|
return unless PlanMyStuff.configuration.reminders_enabled
|
|
24
24
|
|
|
25
25
|
candidates.each do |issue|
|
|
26
|
-
if Closer.should_close?(issue, now: @now)
|
|
27
|
-
Closer.new(issue, now: @now).call
|
|
28
|
-
elsif Fire.ready?(issue, now: @now)
|
|
29
|
-
Fire.new(issue, now: @now).call
|
|
26
|
+
if PlanMyStuff::Reminders::Closer.should_close?(issue, now: @now)
|
|
27
|
+
PlanMyStuff::Reminders::Closer.new(issue, now: @now).call
|
|
28
|
+
elsif PlanMyStuff::Reminders::Fire.ready?(issue, now: @now)
|
|
29
|
+
PlanMyStuff::Reminders::Fire.new(issue, now: @now).call
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
data/lib/plan_my_stuff/repo.rb
CHANGED
|
@@ -16,11 +16,15 @@ module PlanMyStuff
|
|
|
16
16
|
class << self
|
|
17
17
|
# Builds a Repo instance from a Symbol key, full name String, or nil (default).
|
|
18
18
|
#
|
|
19
|
+
# @raise [PlanMyStuff::ConfigurationError] if repo is not provided and cannot be resolved from config
|
|
20
|
+
# @raise [ArgumentError] if repo cannot be resolved
|
|
21
|
+
# @raise [ArgumentError] if repo is invalid format
|
|
22
|
+
#
|
|
19
23
|
# @param repo [Symbol, String, PlanMyStuff::Repo, nil]
|
|
20
24
|
#
|
|
21
25
|
# @return [PlanMyStuff::Repo]
|
|
22
26
|
#
|
|
23
|
-
def resolve(repo = nil)
|
|
27
|
+
def resolve!(repo = nil)
|
|
24
28
|
return repo if repo.is_a?(PlanMyStuff::Repo)
|
|
25
29
|
|
|
26
30
|
repo ||= PlanMyStuff.configuration.default_repo
|
|
@@ -38,10 +42,10 @@ module PlanMyStuff
|
|
|
38
42
|
full_name = PlanMyStuff.configuration.repos[repo]
|
|
39
43
|
raise(ArgumentError, "Unknown repo key: #{repo.inspect}") if full_name.nil?
|
|
40
44
|
|
|
41
|
-
from_full_name(full_name, key: repo)
|
|
45
|
+
from_full_name!(full_name, key: repo)
|
|
42
46
|
when String
|
|
43
47
|
key = PlanMyStuff.configuration.repos.key(repo)
|
|
44
|
-
from_full_name(repo, key: key)
|
|
48
|
+
from_full_name!(repo, key: key)
|
|
45
49
|
else
|
|
46
50
|
raise(ArgumentError, "Cannot resolve repo: #{repo.inspect}")
|
|
47
51
|
end
|
|
@@ -49,12 +53,14 @@ module PlanMyStuff
|
|
|
49
53
|
|
|
50
54
|
private
|
|
51
55
|
|
|
56
|
+
# @raise [ArgumentError] if full_name is not in "Org/Repo" format
|
|
57
|
+
#
|
|
52
58
|
# @param full_name [String] e.g. "YourOrgName/MyRepository"
|
|
53
59
|
# @param key [Symbol, nil]
|
|
54
60
|
#
|
|
55
61
|
# @return [PlanMyStuff::Repo]
|
|
56
62
|
#
|
|
57
|
-
def from_full_name(full_name, key: nil)
|
|
63
|
+
def from_full_name!(full_name, key: nil)
|
|
58
64
|
org, name = full_name.split('/', 2)
|
|
59
65
|
|
|
60
66
|
raise(ArgumentError, "Invalid repo full_name: #{full_name.inspect}") if name.nil?
|
|
@@ -81,8 +87,8 @@ module PlanMyStuff
|
|
|
81
87
|
# @see #full_name
|
|
82
88
|
alias to_s full_name
|
|
83
89
|
|
|
84
|
-
# Enables implicit string coercion so Repo instances behave as
|
|
85
|
-
#
|
|
90
|
+
# Enables implicit string coercion so Repo instances behave as strings when passed to Octokit or compared with
|
|
91
|
+
# String#==.
|
|
86
92
|
#
|
|
87
93
|
# @see #full_name
|
|
88
94
|
alias to_str full_name
|
|
@@ -306,7 +306,7 @@ module PlanMyStuff
|
|
|
306
306
|
|
|
307
307
|
expect(match).to(
|
|
308
308
|
be_truthy,
|
|
309
|
-
"Expected
|
|
309
|
+
"Expected PlanMyStuff #{description} with #{filters.inspect}, " \
|
|
310
310
|
"but recorded actions were: #{format_actions(type)}",
|
|
311
311
|
)
|
|
312
312
|
end
|
|
@@ -335,7 +335,7 @@ module PlanMyStuff
|
|
|
335
335
|
# @return [void]
|
|
336
336
|
#
|
|
337
337
|
def test_mode!
|
|
338
|
-
TestHelpers.recorded_actions = []
|
|
338
|
+
PlanMyStuff::TestHelpers.recorded_actions = []
|
|
339
339
|
return if @_test_mode
|
|
340
340
|
|
|
341
341
|
@_test_mode_originals = {}
|
|
@@ -367,7 +367,7 @@ module PlanMyStuff
|
|
|
367
367
|
|
|
368
368
|
@_test_mode_originals = nil
|
|
369
369
|
@_test_mode = false
|
|
370
|
-
TestHelpers.recorded_actions = []
|
|
370
|
+
PlanMyStuff::TestHelpers.recorded_actions = []
|
|
371
371
|
end
|
|
372
372
|
|
|
373
373
|
private
|
|
@@ -400,7 +400,7 @@ module PlanMyStuff
|
|
|
400
400
|
def stub_issue_class_methods!
|
|
401
401
|
issue_mod = PlanMyStuff::Issue
|
|
402
402
|
%i[create! find list update!].each { |m| save_original(issue_mod, m) }
|
|
403
|
-
%i[add_viewers remove_viewers].each { |m| save_instance_original(issue_mod, m) }
|
|
403
|
+
%i[add_viewers! remove_viewers!].each { |m| save_instance_original(issue_mod, m) }
|
|
404
404
|
|
|
405
405
|
issue_mod.define_singleton_method(:create!) do |**params|
|
|
406
406
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
@@ -415,7 +415,7 @@ module PlanMyStuff
|
|
|
415
415
|
|
|
416
416
|
resolved_repo =
|
|
417
417
|
if params[:repo]
|
|
418
|
-
PlanMyStuff::Repo.resolve(params[:repo]).full_name
|
|
418
|
+
PlanMyStuff::Repo.resolve!(params[:repo]).full_name
|
|
419
419
|
else
|
|
420
420
|
'TestOrg/TestRepo'
|
|
421
421
|
end
|
|
@@ -434,7 +434,7 @@ module PlanMyStuff
|
|
|
434
434
|
params: { number: number, repo: repo },
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
resolved_repo = repo ? PlanMyStuff::Repo.resolve(repo).full_name : 'TestOrg/TestRepo'
|
|
437
|
+
resolved_repo = repo ? PlanMyStuff::Repo.resolve!(repo).full_name : 'TestOrg/TestRepo'
|
|
438
438
|
PlanMyStuff::TestHelpers.build_issue(number: number, repo: resolved_repo)
|
|
439
439
|
end
|
|
440
440
|
|
|
@@ -456,7 +456,7 @@ module PlanMyStuff
|
|
|
456
456
|
nil
|
|
457
457
|
end
|
|
458
458
|
|
|
459
|
-
issue_mod.define_method(:add_viewers) do |user_ids:, user: nil|
|
|
459
|
+
issue_mod.define_method(:add_viewers!) do |user_ids:, user: nil|
|
|
460
460
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
461
461
|
type: :viewers_added,
|
|
462
462
|
params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
|
|
@@ -465,7 +465,7 @@ module PlanMyStuff
|
|
|
465
465
|
nil
|
|
466
466
|
end
|
|
467
467
|
|
|
468
|
-
issue_mod.define_method(:remove_viewers) do |user_ids:, user: nil|
|
|
468
|
+
issue_mod.define_method(:remove_viewers!) do |user_ids:, user: nil|
|
|
469
469
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
470
470
|
type: :viewers_removed,
|
|
471
471
|
params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
|
|
@@ -548,9 +548,9 @@ module PlanMyStuff
|
|
|
548
548
|
# @return [void]
|
|
549
549
|
def stub_project_item_class_methods!
|
|
550
550
|
item_mod = PlanMyStuff::ProjectItem
|
|
551
|
-
%i[move_item create! assign].each { |m| save_original(item_mod, m) }
|
|
551
|
+
%i[move_item! create! assign!].each { |m| save_original(item_mod, m) }
|
|
552
552
|
|
|
553
|
-
item_mod.define_singleton_method(:move_item) do |**params|
|
|
553
|
+
item_mod.define_singleton_method(:move_item!) do |**params|
|
|
554
554
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
555
555
|
type: :item_moved,
|
|
556
556
|
params: {
|
|
@@ -589,7 +589,7 @@ module PlanMyStuff
|
|
|
589
589
|
)
|
|
590
590
|
end
|
|
591
591
|
|
|
592
|
-
item_mod.define_singleton_method(:assign) do |**params|
|
|
592
|
+
item_mod.define_singleton_method(:assign!) do |**params|
|
|
593
593
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
594
594
|
type: :item_assigned,
|
|
595
595
|
params: {
|
|
@@ -17,7 +17,7 @@ module PlanMyStuff
|
|
|
17
17
|
# "Test Status" is intentionally a separate field from GitHub's built-in
|
|
18
18
|
# "Status" (which BaseProject and Pipeline hardcode). Using a parallel
|
|
19
19
|
# field lets the QA sign-off flow own its own options (Passed/Failed)
|
|
20
|
-
# without colliding with the board's default Status column
|
|
20
|
+
# without colliding with the board's default Status column - a testing
|
|
21
21
|
# project pointed at a pipeline-tracked repo won't overwrite either.
|
|
22
22
|
BOOTSTRAP_FIELDS = [
|
|
23
23
|
{ name: 'Test Status', data_type: 'SINGLE_SELECT', options: ['Todo', 'In Progress', 'Passed', 'Failed'] },
|
|
@@ -100,12 +100,12 @@ module PlanMyStuff
|
|
|
100
100
|
# Finds a testing project by number. Raises if the project exists but is
|
|
101
101
|
# not a testing project.
|
|
102
102
|
#
|
|
103
|
+
# @raise [ArgumentError] if the project is not a testing project
|
|
104
|
+
#
|
|
103
105
|
# @param number [Integer]
|
|
104
106
|
# @param paginate [Symbol] :auto (default) or :cursor
|
|
105
107
|
# @param cursor [String, nil]
|
|
106
108
|
#
|
|
107
|
-
# @raise [ArgumentError] if the project is not a testing project
|
|
108
|
-
#
|
|
109
109
|
# @return [PlanMyStuff::TestingProject]
|
|
110
110
|
#
|
|
111
111
|
def find(number, paginate: :auto, cursor: nil)
|
|
@@ -142,7 +142,7 @@ module PlanMyStuff
|
|
|
142
142
|
def create_from_template!(title:, description:, readme:, project_metadata:, template_number:)
|
|
143
143
|
project = clone!(source_number: template_number, title: title)
|
|
144
144
|
|
|
145
|
-
serialized_readme = PlanMyStuff::MetadataParser.serialize(project_metadata.to_h, readme)
|
|
145
|
+
serialized_readme = PlanMyStuff::MetadataParser.serialize!(project_metadata.to_h, readme)
|
|
146
146
|
update_input = { projectId: project.id, readme: serialized_readme }
|
|
147
147
|
update_input[:shortDescription] = description if description.present?
|
|
148
148
|
|
|
@@ -177,7 +177,7 @@ module PlanMyStuff
|
|
|
177
177
|
project_id = new_project[:id]
|
|
178
178
|
project_number = new_project[:number]
|
|
179
179
|
|
|
180
|
-
serialized_readme = PlanMyStuff::MetadataParser.serialize(project_metadata.to_h, readme)
|
|
180
|
+
serialized_readme = PlanMyStuff::MetadataParser.serialize!(project_metadata.to_h, readme)
|
|
181
181
|
update_input = { projectId: project_id, readme: serialized_readme }
|
|
182
182
|
update_input[:shortDescription] = description if description.present?
|
|
183
183
|
|
|
@@ -199,8 +199,9 @@ module PlanMyStuff
|
|
|
199
199
|
# @return [void]
|
|
200
200
|
#
|
|
201
201
|
def bootstrap_fields!(project_id, project_number)
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
org = PlanMyStuff.configuration.organization
|
|
203
|
+
page = fetch_project_page(org, project_number, nil)
|
|
204
|
+
existing_names = (page[:raw].dig(:fields, :nodes) || []).pluck(:name)
|
|
204
205
|
|
|
205
206
|
BOOTSTRAP_FIELDS.each do |field_def|
|
|
206
207
|
next if existing_names.include?(field_def[:name])
|
|
@@ -246,14 +247,14 @@ module PlanMyStuff
|
|
|
246
247
|
|
|
247
248
|
# Returns the Test Status single-select field definition.
|
|
248
249
|
#
|
|
249
|
-
# @return [Hash] with :id and :options keys
|
|
250
|
-
#
|
|
251
250
|
# @raise [PlanMyStuff::APIError] if no Test Status field exists
|
|
252
251
|
#
|
|
253
|
-
|
|
252
|
+
# @return [Hash] with :id and :options keys
|
|
253
|
+
#
|
|
254
|
+
def status_field!
|
|
254
255
|
field = fields.find { |f| f[:name] == 'Test Status' && f[:options] }
|
|
255
256
|
|
|
256
|
-
raise(APIError, "No 'Test Status' field found on project ##{number}") unless field
|
|
257
|
+
raise(PlanMyStuff::APIError, "No 'Test Status' field found on project ##{number}") unless field
|
|
257
258
|
|
|
258
259
|
field
|
|
259
260
|
end
|
|
@@ -25,7 +25,7 @@ module PlanMyStuff
|
|
|
25
25
|
#
|
|
26
26
|
def create!(issue_or_title, draft: false, body: nil, project_number: nil, user: nil)
|
|
27
27
|
item = super
|
|
28
|
-
move_item(project_number: item.project.number, item_id: item.id, status: DEFAULT_STATUS)
|
|
28
|
+
move_item!(project_number: item.project.number, item_id: item.id, status: DEFAULT_STATUS)
|
|
29
29
|
update_single_select_field!(
|
|
30
30
|
project_number: item.project.number,
|
|
31
31
|
item_id: item.id,
|
|
@@ -103,7 +103,7 @@ module PlanMyStuff
|
|
|
103
103
|
# @return [Hash] mutation result
|
|
104
104
|
#
|
|
105
105
|
def update_passed_at!(time)
|
|
106
|
-
update_field!('Passed At', time
|
|
106
|
+
update_field!('Passed At', PlanMyStuff.format_time(time))
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Updates the Deadline Miss Reason field on this testing project item.
|
|
@@ -124,14 +124,16 @@ module PlanMyStuff
|
|
|
124
124
|
#
|
|
125
125
|
# No-op when the user has already signed off on this item.
|
|
126
126
|
#
|
|
127
|
+
# @raise [PlanMyStuff::ValidationError] when user is blank
|
|
128
|
+
#
|
|
127
129
|
# @param user [Object] PMS user object of the tester signing off
|
|
128
130
|
#
|
|
129
131
|
# @return [void]
|
|
130
132
|
#
|
|
131
133
|
def mark_passed!(user)
|
|
132
|
-
raise(
|
|
134
|
+
raise(PlanMyStuff::ValidationError, 'No user configured for sign-off.') if user.blank?
|
|
133
135
|
|
|
134
|
-
user_id =
|
|
136
|
+
user_id = PlanMyStuff::UserResolver.user_id(user).to_s
|
|
135
137
|
current = user_ids_from_field('Passed By')
|
|
136
138
|
return if current.include?(user_id)
|
|
137
139
|
|
|
@@ -158,20 +160,20 @@ module PlanMyStuff
|
|
|
158
160
|
# Records +user_id+ as having failed this item, writes +result_notes+,
|
|
159
161
|
# and moves the item to the Failed status.
|
|
160
162
|
#
|
|
163
|
+
# @raise [PlanMyStuff::ValidationError] if result_notes is blank
|
|
164
|
+
#
|
|
161
165
|
# @param user [Object] PMS user object of the tester failing the item
|
|
162
166
|
# @param result_notes [String] required explanation of the failure
|
|
163
167
|
#
|
|
164
|
-
# @raise [PlanMyStuff::ValidationError] if result_notes is blank
|
|
165
|
-
#
|
|
166
168
|
# @return [void]
|
|
167
169
|
#
|
|
168
170
|
def mark_failed!(user, result_notes:)
|
|
169
|
-
raise(
|
|
170
|
-
raise(
|
|
171
|
+
raise(PlanMyStuff::ValidationError, 'Result notes are required when failing an item.') if result_notes.blank?
|
|
172
|
+
raise(PlanMyStuff::ValidationError, 'No user configured for sign-off.') if user.blank?
|
|
171
173
|
|
|
172
174
|
update_result_notes!(result_notes)
|
|
173
175
|
|
|
174
|
-
user_id =
|
|
176
|
+
user_id = PlanMyStuff::UserResolver.user_id(user).to_s
|
|
175
177
|
current = user_ids_from_field('Failed By')
|
|
176
178
|
return if current.include?(user_id)
|
|
177
179
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
# Metadata for testing projects, stored in the GH Project readme field.
|
|
5
5
|
# Detected at read time by kind: "testing" in the serialized metadata blob.
|
|
6
|
-
class TestingProjectMetadata < BaseProjectMetadata
|
|
6
|
+
class TestingProjectMetadata < PlanMyStuff::BaseProjectMetadata
|
|
7
7
|
# @return [Array<String>] PR/issue URLs this testing project covers
|
|
8
8
|
attr_accessor :subject_urls
|
|
9
9
|
# @return [Date, nil] project-level deadline
|
|
@@ -86,7 +86,7 @@ module PlanMyStuff
|
|
|
86
86
|
def to_h
|
|
87
87
|
super.merge(
|
|
88
88
|
subject_urls: subject_urls,
|
|
89
|
-
due_date: due_date
|
|
89
|
+
due_date: PlanMyStuff.format_time(due_date),
|
|
90
90
|
deadline_miss_reason: deadline_miss_reason,
|
|
91
91
|
)
|
|
92
92
|
end
|
|
@@ -167,6 +167,8 @@ module PlanMyStuff
|
|
|
167
167
|
# a time. Stops on the first non-success so state can be
|
|
168
168
|
# investigated without skipping ahead.
|
|
169
169
|
#
|
|
170
|
+
# @raise [PlanMyStuff::Error] when the endpoint returns a non-success HTTP status
|
|
171
|
+
#
|
|
170
172
|
# @return [void]
|
|
171
173
|
#
|
|
172
174
|
def replay_pending(pending, endpoint_url)
|
|
@@ -191,14 +193,18 @@ module PlanMyStuff
|
|
|
191
193
|
)
|
|
192
194
|
|
|
193
195
|
unless response.is_a?(Net::HTTPSuccess)
|
|
194
|
-
raise(' ! non-success; stopping so you can investigate')
|
|
196
|
+
raise(PlanMyStuff::Error, ' ! non-success; stopping so you can investigate')
|
|
195
197
|
end
|
|
196
198
|
|
|
197
199
|
mark_processed(target[:processed_file], id)
|
|
198
200
|
end
|
|
199
201
|
end
|
|
200
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
|
+
#
|
|
201
206
|
# @return [String]
|
|
207
|
+
#
|
|
202
208
|
def hooks_base_path(scope:, repo:)
|
|
203
209
|
case scope
|
|
204
210
|
when :org
|
|
@@ -212,7 +218,10 @@ module PlanMyStuff
|
|
|
212
218
|
end
|
|
213
219
|
end
|
|
214
220
|
|
|
221
|
+
# @raise [PlanMyStuff::Error] when no webhook matches the given URL
|
|
222
|
+
#
|
|
215
223
|
# @return [Integer]
|
|
224
|
+
#
|
|
216
225
|
def resolve_hook_id(scope:, repo:, webhook_url:)
|
|
217
226
|
hooks = gh_get(hooks_base_path(scope: scope, repo: repo), per_page: 100)
|
|
218
227
|
match = hooks.find { |h| h.dig('config', 'url') == webhook_url }
|
|
@@ -247,7 +256,10 @@ module PlanMyStuff
|
|
|
247
256
|
File.open(path, 'a') { |f| f.puts(id) }
|
|
248
257
|
end
|
|
249
258
|
|
|
259
|
+
# @raise [PlanMyStuff::Error] when the endpoint returns a non-success HTTP status
|
|
260
|
+
#
|
|
250
261
|
# @return [void]
|
|
262
|
+
#
|
|
251
263
|
def replay_each(to_process, deliveries_path, endpoint_url, processed_file, interactive:)
|
|
252
264
|
to_process.each.with_index(1) do |summary, index|
|
|
253
265
|
id = summary['id']
|
|
@@ -265,7 +277,7 @@ module PlanMyStuff
|
|
|
265
277
|
)
|
|
266
278
|
|
|
267
279
|
unless response.is_a?(Net::HTTPSuccess)
|
|
268
|
-
raise(' ! non-success; stopping so you can investigate')
|
|
280
|
+
raise(PlanMyStuff::Error, ' ! non-success; stopping so you can investigate')
|
|
269
281
|
end
|
|
270
282
|
|
|
271
283
|
mark_processed(processed_file, id)
|
data/lib/plan_my_stuff.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
3
5
|
require 'active_support/core_ext/array/wrap'
|
|
4
6
|
require 'active_support/core_ext/object/blank'
|
|
5
7
|
|
|
@@ -44,7 +46,7 @@ module PlanMyStuff
|
|
|
44
46
|
class << self
|
|
45
47
|
# @return [PlanMyStuff::Configuration]
|
|
46
48
|
def configuration
|
|
47
|
-
@configuration ||= Configuration.new
|
|
49
|
+
@configuration ||= PlanMyStuff::Configuration.new
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# @return [PlanMyStuff::Configuration]
|
|
@@ -54,7 +56,12 @@ module PlanMyStuff
|
|
|
54
56
|
|
|
55
57
|
# @return [PlanMyStuff::Client]
|
|
56
58
|
def client
|
|
57
|
-
@client ||= Client.new
|
|
59
|
+
@client ||= PlanMyStuff::Client.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [PlanMyStuff::Client]
|
|
63
|
+
def import_client
|
|
64
|
+
@import_client ||= PlanMyStuff::Client.new(importing: true)
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
# Returns the appropriate HTTP 422 status symbol for the current Rails version.
|
|
@@ -70,13 +77,30 @@ module PlanMyStuff
|
|
|
70
77
|
end
|
|
71
78
|
end
|
|
72
79
|
|
|
80
|
+
# Formats a time-ish value as an ISO 8601 string. +Time+ and
|
|
81
|
+
# +DateTime+ are normalized to UTC; +Date+ is serialized as-is.
|
|
82
|
+
#
|
|
83
|
+
# @param value [Time, DateTime, Date, String, nil]
|
|
84
|
+
#
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
#
|
|
87
|
+
def format_time(value)
|
|
88
|
+
return if value.nil?
|
|
89
|
+
return value if value.is_a?(String)
|
|
90
|
+
return value.iso8601 if value.is_a?(Date) && !value.is_a?(DateTime)
|
|
91
|
+
|
|
92
|
+
value.utc.iso8601
|
|
93
|
+
end
|
|
94
|
+
|
|
73
95
|
# Resets the memoized client and configuration. Useful for testing.
|
|
74
96
|
#
|
|
75
97
|
# @return [void]
|
|
76
98
|
#
|
|
77
99
|
def reset!
|
|
78
100
|
exit_test_mode! if defined?(@_test_mode) && @_test_mode
|
|
101
|
+
PlanMyStuff::Client.exit_trace_mode!
|
|
79
102
|
@client = nil
|
|
103
|
+
@import_client = nil
|
|
80
104
|
@configuration = nil
|
|
81
105
|
end
|
|
82
106
|
end
|
|
@@ -18,12 +18,16 @@ namespace :plan_my_stuff do
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
namespace :webhooks do
|
|
21
|
-
desc 'Create an organization webhook (URL=... [EVENTS=ev1,ev2])'
|
|
21
|
+
desc 'Create an organization webhook (URL=... [EVENTS=ev1,ev2]). ' \
|
|
22
|
+
'Raises PlanMyStuff::ConfigurationError if config.webhook_secret is blank.'
|
|
22
23
|
task create_org: :environment do
|
|
23
|
-
url = ENV.fetch('URL') { raise('URL env var is required') }
|
|
24
|
-
|
|
24
|
+
url = ENV.fetch('URL') { raise(ArgumentError, 'URL env var is required') }
|
|
25
|
+
default_events = %w[projects_v2 projects_v2_item projects_v2_status_update]
|
|
26
|
+
events = ENV['EVENTS'].present? ? ENV['EVENTS'].split(',').map(&:strip).compact_blank : default_events
|
|
25
27
|
config = PlanMyStuff.configuration
|
|
26
|
-
|
|
28
|
+
if config.webhook_secret.blank?
|
|
29
|
+
raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.webhook_secret is blank')
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
hook = PlanMyStuff.client.rest(
|
|
29
33
|
:create_org_hook,
|
|
@@ -36,16 +40,21 @@ namespace :plan_my_stuff do
|
|
|
36
40
|
puts("Events: #{events.join(', ')}")
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
desc 'Create a repo webhook (REPO=owner/name URL=... [EVENTS=ev1,ev2])'
|
|
43
|
+
desc 'Create a repo webhook (REPO=owner/name URL=... [EVENTS=ev1,ev2]). ' \
|
|
44
|
+
'Raises PlanMyStuff::ConfigurationError if config.webhook_secret is blank.'
|
|
40
45
|
task create_repo: :environment do
|
|
41
|
-
url = ENV.fetch('URL') { raise('URL env var is required') }
|
|
46
|
+
url = ENV.fetch('URL') { raise(ArgumentError, 'URL env var is required') }
|
|
42
47
|
repo = ENV.fetch('REPO') do
|
|
43
|
-
PlanMyStuff.client.resolve_repo
|
|
44
|
-
|
|
48
|
+
PlanMyStuff.client.resolve_repo!
|
|
49
|
+
rescue
|
|
50
|
+
raise(ArgumentError, 'REPO env var is required or configured (e.g. BrandsInsurance/PlanMyStuff)')
|
|
45
51
|
end
|
|
46
|
-
|
|
52
|
+
default_events = %w[pull_request issues]
|
|
53
|
+
events = ENV['EVENTS'].present? ? ENV['EVENTS'].split(',').map(&:strip).compact_blank : default_events
|
|
47
54
|
config = PlanMyStuff.configuration
|
|
48
|
-
|
|
55
|
+
if config.webhook_secret.blank?
|
|
56
|
+
raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.webhook_secret is blank')
|
|
57
|
+
end
|
|
49
58
|
|
|
50
59
|
hook = PlanMyStuff.client.rest(
|
|
51
60
|
:create_hook,
|
|
@@ -64,8 +73,8 @@ namespace :plan_my_stuff do
|
|
|
64
73
|
task replay: :environment do
|
|
65
74
|
require 'plan_my_stuff/webhook_replayer'
|
|
66
75
|
|
|
67
|
-
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
68
|
-
webhook_url = ENV.fetch('WEBHOOK_URL') { raise('WEBHOOK_URL env var is required') }
|
|
76
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise(ArgumentError, 'ENDPOINT_URL env var is required') }
|
|
77
|
+
webhook_url = ENV.fetch('WEBHOOK_URL') { raise(ArgumentError, 'WEBHOOK_URL env var is required') }
|
|
69
78
|
scope = ENV.fetch('SCOPE', 'org').to_sym
|
|
70
79
|
repo = ENV.fetch('REPO', nil)
|
|
71
80
|
processed_file =
|
|
@@ -85,11 +94,12 @@ namespace :plan_my_stuff do
|
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
desc 'Continuously poll org + repo hooks and auto-replay new deliveries ' \
|
|
88
|
-
'(ENDPOINT_URL=... [ORG_WEBHOOK_URL=...] [REPO_WEBHOOK_URL=... REPO=owner/name] [INTERVAL=15])'
|
|
97
|
+
'(ENDPOINT_URL=... [ORG_WEBHOOK_URL=...] [REPO_WEBHOOK_URL=... REPO=owner/name] [INTERVAL=15]). ' \
|
|
98
|
+
'At least one of ORG_WEBHOOK_URL or REPO_WEBHOOK_URL is required (raises ArgumentError if both absent).'
|
|
89
99
|
task listen: :environment do
|
|
90
100
|
require 'plan_my_stuff/webhook_replayer'
|
|
91
101
|
|
|
92
|
-
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
102
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise(ArgumentError, 'ENDPOINT_URL env var is required') }
|
|
93
103
|
org_webhook_url = ENV.fetch('ORG_WEBHOOK_URL', nil)
|
|
94
104
|
repo_webhook_url = ENV.fetch('REPO_WEBHOOK_URL', nil)
|
|
95
105
|
repo = ENV.fetch('REPO', nil)
|
|
@@ -98,11 +108,11 @@ namespace :plan_my_stuff do
|
|
|
98
108
|
targets = []
|
|
99
109
|
targets << { scope: :org, webhook_url: org_webhook_url } if org_webhook_url.present?
|
|
100
110
|
if repo_webhook_url.present?
|
|
101
|
-
raise('REPO env var required when REPO_WEBHOOK_URL is set') if repo.blank?
|
|
111
|
+
raise(ArgumentError, 'REPO env var required when REPO_WEBHOOK_URL is set') if repo.blank?
|
|
102
112
|
|
|
103
113
|
targets << { scope: :repo, webhook_url: repo_webhook_url, repo: repo }
|
|
104
114
|
end
|
|
105
|
-
raise('Set at least one of ORG_WEBHOOK_URL or REPO_WEBHOOK_URL') if targets.empty?
|
|
115
|
+
raise(ArgumentError, 'Set at least one of ORG_WEBHOOK_URL or REPO_WEBHOOK_URL') if targets.empty?
|
|
106
116
|
|
|
107
117
|
processed_file_for = -> (target) {
|
|
108
118
|
Rails.root.join('tmp', 'plan_my_stuff', "webhook_listen_#{target[:scope]}_processed.txt").to_s
|
|
@@ -121,10 +131,10 @@ namespace :plan_my_stuff do
|
|
|
121
131
|
task simulate_aws: :environment do
|
|
122
132
|
require 'plan_my_stuff/aws_sns_simulator'
|
|
123
133
|
|
|
124
|
-
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise('ENDPOINT_URL env var is required') }
|
|
134
|
+
endpoint_url = ENV.fetch('ENDPOINT_URL') { raise(ArgumentError, 'ENDPOINT_URL env var is required') }
|
|
125
135
|
event_name = ENV.fetch('EVENT', PlanMyStuff::AwsSnsSimulator::DEFAULT_EVENT)
|
|
126
136
|
|
|
127
|
-
PlanMyStuff::AwsSnsSimulator.post(
|
|
137
|
+
PlanMyStuff::AwsSnsSimulator.post!(
|
|
128
138
|
endpoint_url: endpoint_url,
|
|
129
139
|
event_name: event_name,
|
|
130
140
|
)
|
|
@@ -133,7 +143,8 @@ namespace :plan_my_stuff do
|
|
|
133
143
|
|
|
134
144
|
namespace :reminders do
|
|
135
145
|
desc 'Enqueue a RemindersSweepJob per configured repo ' \
|
|
136
|
-
'([REPO=<key>] to target a single repo)'
|
|
146
|
+
'([REPO=<key>] to target a single repo). ' \
|
|
147
|
+
'Raises PlanMyStuff::ConfigurationError when no repos are configured (config.repos empty and REPO unset).'
|
|
137
148
|
task sweep: :environment do
|
|
138
149
|
config = PlanMyStuff.configuration
|
|
139
150
|
repo_keys =
|
|
@@ -143,7 +154,9 @@ namespace :plan_my_stuff do
|
|
|
143
154
|
config.repos.keys
|
|
144
155
|
end
|
|
145
156
|
|
|
146
|
-
|
|
157
|
+
if repo_keys.empty?
|
|
158
|
+
raise(PlanMyStuff::ConfigurationError, 'No repos configured (set config.repos or pass REPO=<key>)')
|
|
159
|
+
end
|
|
147
160
|
|
|
148
161
|
repo_keys.each do |key|
|
|
149
162
|
PlanMyStuff::RemindersSweepJob.requeue(key)
|
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.8.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-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- app/views/plan_my_stuff/issues/partials/_links.html.erb
|
|
87
87
|
- app/views/plan_my_stuff/issues/partials/_viewers.html.erb
|
|
88
88
|
- app/views/plan_my_stuff/issues/show.html.erb
|
|
89
|
+
- app/views/plan_my_stuff/partials/_flash.html.erb
|
|
89
90
|
- app/views/plan_my_stuff/projects/edit.html.erb
|
|
90
91
|
- app/views/plan_my_stuff/projects/index.html.erb
|
|
91
92
|
- app/views/plan_my_stuff/projects/new.html.erb
|