plan_my_stuff 0.7.0 → 0.9.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/CONFIGURATION.md +351 -0
  4. data/README.md +100 -103
  5. data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
  11. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
  12. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
  13. data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
  14. data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
  15. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
  16. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
  17. data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
  18. data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
  19. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
  20. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
  21. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
  22. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
  23. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
  24. data/app/jobs/plan_my_stuff/application_job.rb +2 -3
  25. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
  26. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  27. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
  28. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  29. data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
  30. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  31. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
  32. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
  33. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
  34. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
  35. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
  36. data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
  37. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  38. data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
  39. data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
  40. data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
  41. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
  42. data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
  43. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
  44. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
  45. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
  46. data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
  47. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
  48. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
  50. data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
  51. data/config/routes.rb +2 -2
  52. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +52 -14
  53. data/lib/plan_my_stuff/approval.rb +12 -4
  54. data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
  55. data/lib/plan_my_stuff/base_metadata.rb +4 -15
  56. data/lib/plan_my_stuff/base_project.rb +68 -55
  57. data/lib/plan_my_stuff/base_project_item.rb +62 -57
  58. data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
  59. data/lib/plan_my_stuff/client.rb +136 -48
  60. data/lib/plan_my_stuff/comment.rb +59 -57
  61. data/lib/plan_my_stuff/comment_metadata.rb +1 -1
  62. data/lib/plan_my_stuff/configuration.rb +93 -93
  63. data/lib/plan_my_stuff/errors.rb +10 -10
  64. data/lib/plan_my_stuff/graphql/queries.rb +1 -1
  65. data/lib/plan_my_stuff/issue.rb +471 -333
  66. data/lib/plan_my_stuff/issue_metadata.rb +10 -10
  67. data/lib/plan_my_stuff/label.rb +34 -18
  68. data/lib/plan_my_stuff/link.rb +15 -15
  69. data/lib/plan_my_stuff/markdown.rb +12 -6
  70. data/lib/plan_my_stuff/metadata_parser.rb +3 -1
  71. data/lib/plan_my_stuff/notifications.rb +1 -1
  72. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
  73. data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
  74. data/lib/plan_my_stuff/pipeline.rb +61 -83
  75. data/lib/plan_my_stuff/project.rb +4 -4
  76. data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
  77. data/lib/plan_my_stuff/project_metadata.rb +1 -1
  78. data/lib/plan_my_stuff/reminders/closer.rb +1 -1
  79. data/lib/plan_my_stuff/reminders/fire.rb +3 -3
  80. data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
  81. data/lib/plan_my_stuff/repo.rb +12 -6
  82. data/lib/plan_my_stuff/test_helpers.rb +11 -11
  83. data/lib/plan_my_stuff/testing_project.rb +12 -11
  84. data/lib/plan_my_stuff/testing_project_item.rb +11 -9
  85. data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
  86. data/lib/plan_my_stuff/version.rb +1 -1
  87. data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
  88. data/lib/plan_my_stuff.rb +26 -2
  89. data/lib/tasks/plan_my_stuff.rake +33 -20
  90. metadata +4 -2
@@ -1,22 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class ProjectItemsController < ApplicationController
4
+ class ProjectItemsController < PlanMyStuff::ApplicationController
5
5
  # POST /projects/:project_id/items
6
6
  def create
7
7
  project_number = params[:project_id].to_i
8
8
 
9
9
  if params[:draft] == '1'
10
- PMS::ProjectItem.create!(params[:title], draft: true, body: params[:body], project_number: project_number)
10
+ PlanMyStuff::ProjectItem.create!(
11
+ params[:title],
12
+ draft: true,
13
+ body: params[:body],
14
+ project_number: project_number,
15
+ )
11
16
  flash[:success] = 'Draft item created.'
12
17
  else
13
- issue = PMS::Issue.find(params[:issue_number].to_i)
14
- PMS::ProjectItem.create!(issue, project_number: project_number)
18
+ issue = PlanMyStuff::Issue.find(params[:issue_number].to_i)
19
+ PlanMyStuff::ProjectItem.create!(issue, project_number: project_number)
15
20
  flash[:success] = "Issue ##{issue.number} added to project."
16
21
  end
17
22
 
18
23
  redirect_to(plan_my_stuff.project_path(project_number))
19
- rescue ArgumentError, PMS::Error, Octokit::Error => e
24
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
25
+ pms_handle_rescue(e)
26
+ flash[:error] = e.message
27
+ redirect_to(plan_my_stuff.project_path(project_number))
28
+ end
29
+
30
+ # DELETE /projects/:project_id/items/:id
31
+ def destroy
32
+ project_number = params[:project_id].to_i
33
+ item_id = params[:id]
34
+
35
+ project = PlanMyStuff::Project.find(project_number)
36
+ item = project.items.find { |i| i.id == item_id }
37
+ raise(PlanMyStuff::APIError, "Item not found: #{item_id}") if item.nil?
38
+
39
+ item.destroy!(user: pms_current_user)
40
+
41
+ flash[:success] = 'Item removed from project.'
42
+ redirect_to(plan_my_stuff.project_path(project_number))
43
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
44
+ pms_handle_rescue(e)
20
45
  flash[:error] = e.message
21
46
  redirect_to(plan_my_stuff.project_path(project_number))
22
47
  end
@@ -1,28 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class ProjectsController < ApplicationController
4
+ class ProjectsController < PlanMyStuff::ApplicationController
5
5
  # GET /projects
6
6
  def index
7
- all_projects = PMS::BaseProject.list.reject(&:closed)
7
+ all_projects = PlanMyStuff::BaseProject.list.reject(&:closed)
8
8
  @filter = params[:filter].presence_in(%w[testing all]) || 'regular'
9
9
  @projects =
10
10
  case @filter
11
- when 'testing' then all_projects.select { |p| p.is_a?(PMS::TestingProject) }
12
- when 'regular' then all_projects.reject { |p| p.is_a?(PMS::TestingProject) }
11
+ when 'testing' then all_projects.select { |p| p.is_a?(PlanMyStuff::TestingProject) }
12
+ when 'regular' then all_projects.reject { |p| p.is_a?(PlanMyStuff::TestingProject) }
13
13
  else all_projects
14
14
  end
15
- @support_user = support_user?
16
15
  end
17
16
 
18
17
  # GET /projects/new
19
18
  def new
20
- @project = PMS::Project.new
19
+ @project = PlanMyStuff::Project.new
21
20
  end
22
21
 
23
22
  # POST /projects
24
23
  def create
25
- @project = PMS::Project.create!(
24
+ @project = PlanMyStuff::Project.create!(
26
25
  title: project_params[:title],
27
26
  readme: project_params[:readme] || '',
28
27
  description: project_params[:description],
@@ -31,32 +30,32 @@ module PlanMyStuff
31
30
 
32
31
  flash[:success] = 'Project was successfully created.'
33
32
  redirect_to(plan_my_stuff.project_path(@project.number))
34
- rescue PMS::ValidationError => e
35
- @project = PMS::Project.new(
33
+ rescue PlanMyStuff::ValidationError => e
34
+ pms_handle_rescue(e)
35
+ @project = PlanMyStuff::Project.new(
36
36
  title: project_params[:title],
37
37
  readme: project_params[:readme],
38
38
  description: project_params[:description],
39
39
  )
40
40
  flash.now[:error] = e.message
41
- render(:new, status: PMS.unprocessable_status)
41
+ render(:new, status: PlanMyStuff.unprocessable_status)
42
42
  end
43
43
 
44
44
  # GET /projects/:id
45
45
  def show
46
- @project = PMS::Project.find(params[:id].to_i)
46
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
47
47
  @statuses = @project.statuses.pluck(:name)
48
48
  @items_by_status = @project.items.group_by(&:status)
49
- @support_user = support_user?
50
49
  end
51
50
 
52
51
  # GET /projects/:id/edit
53
52
  def edit
54
- @project = PMS::Project.find(params[:id].to_i)
53
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
55
54
  end
56
55
 
57
56
  # PATCH/PUT /projects/:id
58
57
  def update
59
- @project = PMS::Project.find(params[:id].to_i)
58
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
60
59
 
61
60
  @project.update!(
62
61
  title: project_params[:title],
@@ -66,9 +65,10 @@ module PlanMyStuff
66
65
 
67
66
  flash[:success] = 'Project was successfully updated.'
68
67
  redirect_to(plan_my_stuff.project_path(@project.number))
69
- rescue PMS::StaleObjectError
68
+ rescue PlanMyStuff::StaleObjectError => e
69
+ pms_handle_rescue(e)
70
70
  flash.now[:error] = 'Project was modified by someone else. Please review the latest changes and try again.'
71
- render(:edit, status: PMS.unprocessable_status)
71
+ render(:edit, status: PlanMyStuff.unprocessable_status)
72
72
  end
73
73
 
74
74
  private
@@ -7,7 +7,7 @@ module PlanMyStuff
7
7
  # Pass is submitted directly via button_to with result: "pass".
8
8
  # Fail renders a form (new) to capture result_notes before posting to create.
9
9
  #
10
- class ResultsController < ApplicationController
10
+ class ResultsController < PlanMyStuff::ApplicationController
11
11
  # GET /testing_projects/:testing_project_id/items/:item_id/result/new
12
12
  def new
13
13
  @project_number = params[:testing_project_id].to_i
@@ -19,21 +19,31 @@ module PlanMyStuff
19
19
  project_number = params[:testing_project_id].to_i
20
20
  item = find_project_item(project_number)
21
21
 
22
- if params[:result] == 'pass'
22
+ case params[:result]
23
+ when 'pass'
23
24
  item.mark_passed!(pms_current_user)
24
25
  flash[:success] = 'Item marked as Passed.'
25
- else
26
+ when 'fail'
26
27
  item.mark_failed!(pms_current_user, result_notes: params[:result_notes])
27
28
  flash[:success] = 'Item marked as Failed.'
29
+ else
30
+ raise(ArgumentError, "Invalid result: #{params[:result].inspect}")
28
31
  end
29
32
 
30
33
  redirect_to(plan_my_stuff.testing_project_path(project_number))
31
- rescue PMS::ValidationError => e
32
- flash.now[:error] = e.message
33
- @project_number = params[:testing_project_id].to_i
34
- @item_id = params[:item_id]
35
- render(:new, status: PMS.unprocessable_status)
36
- rescue ArgumentError, PMS::Error, Octokit::Error => e
34
+ rescue PlanMyStuff::ValidationError => e
35
+ pms_handle_rescue(e)
36
+ if params[:result] == 'fail'
37
+ flash.now[:error] = e.message
38
+ @project_number = params[:testing_project_id].to_i
39
+ @item_id = params[:item_id]
40
+ render(:new, status: PlanMyStuff.unprocessable_status)
41
+ else
42
+ flash[:error] = e.message
43
+ redirect_to(plan_my_stuff.testing_project_path(params[:testing_project_id].to_i))
44
+ end
45
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
46
+ pms_handle_rescue(e)
37
47
  flash[:error] = e.message
38
48
  redirect_to(plan_my_stuff.testing_project_path(params[:testing_project_id].to_i))
39
49
  end
@@ -43,9 +53,9 @@ module PlanMyStuff
43
53
  # @param project_number [Integer]
44
54
  # @return [PlanMyStuff::TestingProjectItem]
45
55
  def find_project_item(project_number)
46
- project = PMS::TestingProject.find(project_number)
56
+ project = PlanMyStuff::TestingProject.find(project_number)
47
57
  item = project.items.find { |i| i.id == params[:item_id] }
48
- raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
58
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
49
59
 
50
60
  item
51
61
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class TestingProjectItemsController < ApplicationController
4
+ class TestingProjectItemsController < PlanMyStuff::ApplicationController
5
5
  # GET /testing_projects/:testing_project_id/items/new
6
6
  def new
7
- @project = PMS::TestingProject.find(params[:testing_project_id].to_i)
7
+ @project = PlanMyStuff::TestingProject.find(params[:testing_project_id].to_i)
8
+ rescue PlanMyStuff::Error, Octokit::Error => e
9
+ pms_handle_rescue(e)
10
+ flash[:error] = e.message
11
+ redirect_to(plan_my_stuff.testing_projects_path)
8
12
  end
9
13
 
10
14
  # POST /testing_projects/:testing_project_id/items
11
15
  def create
12
16
  project_number = params[:testing_project_id].to_i
13
- item = PMS::TestingProjectItem.create!(
17
+ item = PlanMyStuff::TestingProjectItem.create!(
14
18
  item_params[:title],
15
19
  draft: true,
16
20
  body: item_params[:body],
@@ -24,7 +28,8 @@ module PlanMyStuff
24
28
 
25
29
  flash[:success] = 'Item added.'
26
30
  redirect_to(plan_my_stuff.testing_project_path(project_number))
27
- rescue ArgumentError, PMS::Error, Octokit::Error => e
31
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
32
+ pms_handle_rescue(e)
28
33
  flash[:error] = e.message
29
34
  redirect_to(plan_my_stuff.testing_project_path(project_number))
30
35
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class TestingProjectsController < ApplicationController
4
+ class TestingProjectsController < PlanMyStuff::ApplicationController
5
5
  # GET /testing_projects/new
6
6
  def new
7
- @project = PMS::TestingProject.new
7
+ @project = PlanMyStuff::TestingProject.new
8
8
  @project.metadata.subject_urls = [params[:subject_url]] if params[:subject_url].present?
9
9
  end
10
10
 
11
11
  # POST /testing_projects
12
12
  def create
13
- @project = PMS::TestingProject.create!(
13
+ @project = PlanMyStuff::TestingProject.create!(
14
14
  title: testing_project_params[:title],
15
15
  description: testing_project_params[:description],
16
16
  subject_urls: parse_subject_urls(testing_project_params[:subject_urls]),
@@ -21,28 +21,34 @@ module PlanMyStuff
21
21
 
22
22
  flash[:success] = 'Testing project was successfully created.'
23
23
  redirect_to(plan_my_stuff.testing_project_path(@project.number))
24
- rescue PMS::ValidationError => e
25
- @project = PMS::TestingProject.new(title: testing_project_params[:title])
24
+ rescue PlanMyStuff::ValidationError => e
25
+ pms_handle_rescue(e)
26
+ @project = PlanMyStuff::TestingProject.new(
27
+ title: testing_project_params[:title],
28
+ description: testing_project_params[:description],
29
+ )
30
+ @project.metadata.subject_urls = parse_subject_urls(testing_project_params[:subject_urls])
31
+ @project.metadata.due_date = safe_parse_due_date(testing_project_params[:due_date])
32
+ @project.metadata.deadline_miss_reason = testing_project_params[:deadline_miss_reason]
26
33
  flash.now[:error] = e.message
27
- render(:new, status: PMS.unprocessable_status)
34
+ render(:new, status: PlanMyStuff.unprocessable_status)
28
35
  end
29
36
 
30
37
  # GET /testing_projects/:id
31
38
  def show
32
- @project = PMS::TestingProject.find(params[:id].to_i)
39
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
33
40
  @statuses = @project.statuses.pluck(:name)
34
41
  @items_by_status = @project.items.group_by(&:status)
35
- @support_user = support_user?
36
42
  end
37
43
 
38
44
  # GET /testing_projects/:id/edit
39
45
  def edit
40
- @project = PMS::TestingProject.find(params[:id].to_i)
46
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
41
47
  end
42
48
 
43
49
  # PATCH/PUT /testing_projects/:id
44
50
  def update
45
- @project = PMS::TestingProject.find(params[:id].to_i)
51
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
46
52
 
47
53
  @project.update!(
48
54
  title: testing_project_params[:title],
@@ -56,13 +62,15 @@ module PlanMyStuff
56
62
 
57
63
  flash[:success] = 'Testing project was successfully updated.'
58
64
  redirect_to(plan_my_stuff.testing_project_path(@project.number))
59
- rescue PMS::StaleObjectError
65
+ rescue PlanMyStuff::StaleObjectError => e
66
+ pms_handle_rescue(e)
60
67
  flash.now[:error] =
61
68
  'Testing project was modified by someone else. Please review the latest changes and try again.'
62
- render(:edit, status: PMS.unprocessable_status)
63
- rescue PMS::ValidationError => e
69
+ render(:edit, status: PlanMyStuff.unprocessable_status)
70
+ rescue PlanMyStuff::ValidationError => e
71
+ pms_handle_rescue(e)
64
72
  flash.now[:error] = e.message
65
- render(:edit, status: PMS.unprocessable_status)
73
+ render(:edit, status: PlanMyStuff.unprocessable_status)
66
74
  end
67
75
 
68
76
  private
@@ -80,6 +88,14 @@ module PlanMyStuff
80
88
 
81
89
  Date.parse(raw)
82
90
  rescue ArgumentError, TypeError
91
+ raise(PlanMyStuff::ValidationError, 'Due date must be a valid date.')
92
+ end
93
+
94
+ # @param raw [String, nil]
95
+ # @return [Date, nil]
96
+ def safe_parse_due_date(raw)
97
+ parse_due_date(raw)
98
+ rescue PlanMyStuff::ValidationError
83
99
  nil
84
100
  end
85
101
 
@@ -5,7 +5,7 @@ module PlanMyStuff
5
5
  class AwsController < ActionController::API
6
6
  VALID_SNS_MESSAGE_TYPES = %w[Notification SubscriptionConfirmation UnsubscribeConfirmation].freeze
7
7
 
8
- before_action :verify_signature!
8
+ before_action :verify_signature
9
9
 
10
10
  # POST /webhooks/aws
11
11
  def create
@@ -14,6 +14,13 @@ module PlanMyStuff
14
14
 
15
15
  Rails.logger.info("[PlanMyStuff] SNS #{message_type}: #{sns_params.to_unsafe_h.inspect}")
16
16
 
17
+ if message_type == 'SubscriptionConfirmation'
18
+ confirm_subscription!
19
+ head(:ok)
20
+
21
+ return
22
+ end
23
+
17
24
  unless message_type == 'Notification'
18
25
  head(:ok)
19
26
 
@@ -42,14 +49,36 @@ module PlanMyStuff
42
49
 
43
50
  private
44
51
 
52
+ # Visits the SubscribeURL provided by SNS to confirm the subscription. Without this, SNS leaves the
53
+ # endpoint in +PendingConfirmation+ and never delivers Notifications.
54
+ #
55
+ # @return [void]
56
+ #
57
+ def confirm_subscription!
58
+ subscribe_url = sns_params[:subscribe_url].to_s
59
+ if subscribe_url.blank?
60
+ Rails.logger.warn('[PlanMyStuff] SubscriptionConfirmation missing SubscribeURL')
61
+
62
+ return
63
+ end
64
+
65
+ uri = URI.parse(subscribe_url)
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.use_ssl = (uri.scheme == 'https')
68
+ response = http.request(Net::HTTP::Get.new(uri.request_uri))
69
+
70
+ Rails.logger.info("[PlanMyStuff] Confirmed SNS subscription: HTTP #{response.code}")
71
+ rescue => e
72
+ Rails.logger.error("[PlanMyStuff] SNS subscription confirmation failed: #{e.class}: #{e.message}")
73
+ end
74
+
45
75
  # Validates the SNS message signature.
46
- # Checks message type header and topic ARN first (cheap), then
47
- # delegates to the configured verifier class for cryptographic
48
- # certificate-based verification.
76
+ # Checks message type header and topic ARN first (cheap), then delegates to the configured verifier class
77
+ # for cryptographic certificate-based verification.
49
78
  #
50
79
  # @return [void]
51
80
  #
52
- def verify_signature!
81
+ def verify_signature
53
82
  message_type = request.headers['x-amz-sns-message-type'].to_s
54
83
 
55
84
  if VALID_SNS_MESSAGE_TYPES.exclude?(message_type)
@@ -82,9 +111,8 @@ module PlanMyStuff
82
111
  end
83
112
  end
84
113
 
85
- # Parses the SNS envelope. Returns empty params if the body is
86
- # not valid JSON so signature verification rejects the request
87
- # with 401 instead of bubbling a 500.
114
+ # Parses the SNS envelope. Returns empty params if the body is not valid JSON so signature verification
115
+ # rejects the request with 401 instead of bubbling a 500.
88
116
  #
89
117
  # @return [ActionController::Parameters]
90
118
  #
@@ -94,8 +122,8 @@ module PlanMyStuff
94
122
  @sns_params ||= ActionController::Parameters.new
95
123
  end
96
124
 
97
- # Parses the nested SNS message payload. Returns empty params on
98
- # malformed JSON so callers degrade gracefully.
125
+ # Parses the nested SNS message payload. Returns empty params on malformed JSON so callers degrade
126
+ # gracefully.
99
127
  #
100
128
  # @return [ActionController::Parameters]
101
129
  #
@@ -106,8 +134,7 @@ module PlanMyStuff
106
134
  ActionController::Parameters.new
107
135
  end
108
136
 
109
- # Checks whether any resource ARN in the ECS event matches the
110
- # configured service identifier suffix.
137
+ # Checks whether any resource ARN in the ECS event matches the configured service identifier suffix.
111
138
  #
112
139
  # @param service_identifier [String, nil]
113
140
  #
@@ -119,15 +146,21 @@ module PlanMyStuff
119
146
  sns_message_params.fetch(:resources, []).any? { |arn| arn.end_with?(service_identifier) }
120
147
  end
121
148
 
122
- # Finds "Release in Progress" items whose linked issue commit SHA
123
- # matches the configured production commit SHA (prefix match),
124
- # then completes deployment for each.
149
+ # Finds "Release in Progress" items whose linked issue commit SHA matches the configured production
150
+ # commit SHA (prefix match), then completes deployment for each.
125
151
  #
126
152
  # @return [void]
127
153
  #
128
154
  def handle_deployment_completed
129
155
  sha = PlanMyStuff.configuration.production_commit_sha
130
- raise(PlanMyStuff::PipelineError, 'production_commit_sha is not configured') if sha.nil?
156
+
157
+ if sha.blank?
158
+ Rails.logger.error(
159
+ '[PlanMyStuff] production_commit_sha is not configured - skipping deployment completion',
160
+ )
161
+
162
+ return
163
+ end
131
164
 
132
165
  release_in_progress_items.each do |item|
133
166
  next if item.draft?
@@ -143,7 +176,7 @@ module PlanMyStuff
143
176
  # @return [Array<PlanMyStuff::ProjectItem>]
144
177
  #
145
178
  def release_in_progress_items
146
- project_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
179
+ project_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
147
180
  project = PlanMyStuff::Project.find(project_number)
148
181
 
149
182
  release_status = PlanMyStuff::Pipeline.resolve_status_name(
@@ -5,7 +5,7 @@ require 'openssl'
5
5
  module PlanMyStuff
6
6
  module Webhooks
7
7
  class GithubController < ActionController::API
8
- before_action :verify_signature!
8
+ before_action :verify_signature
9
9
 
10
10
  # POST /webhooks/github
11
11
  def create
@@ -29,7 +29,7 @@ module PlanMyStuff
29
29
  #
30
30
  # @return [void]
31
31
  #
32
- def verify_signature!
32
+ def verify_signature
33
33
  body = request.body.read
34
34
  request.body.rewind
35
35
 
@@ -81,21 +81,15 @@ module PlanMyStuff
81
81
  end
82
82
  end
83
83
 
84
- # Adds the issue to the pipeline project at "Started" the first
85
- # time it is assigned. Re-assigns and additional assignees are
86
- # ignored -- if the issue is already in the pipeline we do not
87
- # touch its status.
84
+ # Adds the issue to the pipeline project at "Started" the first time it is assigned. Re-assigns and
85
+ # additional assignees are ignored -- if the issue is already in the pipeline we do not touch its status.
88
86
  #
89
87
  # Skipped paths (no project item is created):
90
- # - Issue is closed (assignment changes on a closed issue
91
- # must not alter the pipeline)
92
- # - Issue has pending approvals (creating + +take!+ would
93
- # either orphan an item or 500 on the approval guard)
88
+ # - Issue is closed (assignment changes on a closed issue must not alter the pipeline)
89
+ # - Issue has pending approvals (creating + +take!+ would either orphan an item or 500 on the approval guard)
94
90
  #
95
- # GitHub already records the issue assignment (that's what
96
- # triggered this webhook), so the gem does not call +assign!+
97
- # on the project item -- that would clobber co-assignees on
98
- # the underlying issue.
91
+ # GitHub already records the issue assignment (that's what triggered this webhook), so the gem does not
92
+ # call +assign!+ on the project item -- that would clobber co-assignees on the underlying issue.
99
93
  #
100
94
  # @return [void]
101
95
  #
@@ -123,21 +117,18 @@ module PlanMyStuff
123
117
  return
124
118
  end
125
119
 
126
- number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
120
+ number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
127
121
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
128
122
  PlanMyStuff::Pipeline.take!(project_item)
129
123
  end
130
124
 
131
- # Removes the issue from the pipeline project when the LAST
132
- # assignee is removed. If any assignees remain, the webhook is
133
- # a no-op (only one of N was removed). If the issue isn't in
134
- # the pipeline at all, also a no-op (logged at info).
125
+ # Removes the issue from the pipeline project when the LAST assignee is removed. If any assignees remain,
126
+ # the webhook is a no-op (only one of N was removed). If the issue isn't in the pipeline at all, also a
127
+ # no-op (logged at info).
135
128
  #
136
- # Closed issues are skipped: assignment changes on a closed
137
- # issue must not alter the pipeline. Items already on the
138
- # release cycle (Ready for Release, Release in Progress,
139
- # Completed) are also locked -- once on the release path an
140
- # item should not come off via webhook.
129
+ # Closed issues are skipped: assignment changes on a closed issue must not alter the pipeline. Items
130
+ # already on the release cycle (Ready for Release, Release in Progress, Completed) are also locked --
131
+ # once on the release path an item should not come off via webhook.
141
132
  #
142
133
  # @return [void]
143
134
  #
@@ -145,7 +136,7 @@ module PlanMyStuff
145
136
  return if issue_params[:state] == 'closed'
146
137
 
147
138
  remaining_assignees = Array.wrap(issue_params[:assignees])
148
- return if remaining_assignees.any?
139
+ return if remaining_assignees.present?
149
140
 
150
141
  issue_number = issue_params.fetch(:number)
151
142
  project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
@@ -169,16 +160,13 @@ module PlanMyStuff
169
160
  PlanMyStuff::Pipeline.remove!(project_item)
170
161
  end
171
162
 
172
- # Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev
173
- # dragging an item from "Submitted" to "Started" on the project
174
- # board into a +Pipeline.take!+ call so the
175
- # +plan_my_stuff.pipeline.started+ event fires.
163
+ # Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev dragging an item from "Submitted" to
164
+ # "Started" on the project board into a +Pipeline.take!+ call so the +plan_my_stuff.pipeline.started+
165
+ # event fires.
176
166
  #
177
- # Only acts on +action: 'edited'+ where the Status single-select
178
- # field changed on the pipeline project. No-ops when the new
179
- # status is anything other than "Started", when the +from+ status
180
- # is already "Started" (loop guard from +move_to!+ triggering
181
- # another webhook), or when the item cannot be located on the
167
+ # Only acts on +action: 'edited'+ where the Status single-select field changed on the pipeline project.
168
+ # No-ops when the new status is anything other than "Started", when the +from+ status is already "Started"
169
+ # (loop guard from +move_to!+ triggering another webhook), or when the item cannot be located on the
182
170
  # pipeline project.
183
171
  #
184
172
  # @return [void]
@@ -189,7 +177,7 @@ module PlanMyStuff
189
177
  item_project_node_id = payload_params.dig(:projects_v2_item, :project_node_id)
190
178
  return if item_project_node_id.blank?
191
179
 
192
- pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
180
+ pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
193
181
  project = PlanMyStuff::Project.find(pipeline_number)
194
182
  return unless project.id == item_project_node_id
195
183
 
@@ -231,11 +219,9 @@ module PlanMyStuff
231
219
  def handle_pull_request
232
220
  action = payload_params.fetch(:action)
233
221
 
234
- # Only PRs targeting +main+ drive pipeline transitions on
235
- # open/draft/reopen. PRs into +production+ are deploy PRs
236
- # (often auto-created as drafts) and must not bump items
237
- # back to "Started". The +closed+ branch handles its own
238
- # base-ref routing.
222
+ # Only PRs targeting +main+ drive pipeline transitions on open/draft/reopen. PRs into +production+ are
223
+ # deploy PRs (often auto-created as drafts) and must not bump items back to "Started". The +closed+
224
+ # branch handles its own base-ref routing.
239
225
  if %w[opened ready_for_review reopened converted_to_draft].include?(action)
240
226
  base_ref = pull_request_params.dig(:base, :ref)
241
227
  return unless base_ref == PlanMyStuff.configuration.main_branch
@@ -253,11 +239,9 @@ module PlanMyStuff
253
239
  end
254
240
  end
255
241
 
256
- # Opening a PR as a draft is a soft "I've started working on this"
257
- # signal. For each linked issue, when the issue is not yet in the
258
- # pipeline, add it at "Started"; when the issue has no assignees,
259
- # assign the PR author. Already-in-pipeline items are NOT moved
260
- # (a draft open is not a status change).
242
+ # Opening a PR as a draft is a soft "I've started working on this" signal. For each linked issue, when
243
+ # the issue is not yet in the pipeline, add it at "Started"; when the issue has no assignees, assign the
244
+ # PR author. Already-in-pipeline items are NOT moved (a draft open is not a status change).
261
245
  #
262
246
  # @return [void]
263
247
  #
@@ -283,9 +267,8 @@ module PlanMyStuff
283
267
  end
284
268
  end
285
269
 
286
- # Adds the issue to the pipeline at "Started". Skipped when the
287
- # issue has pending approvals (Pipeline.take!'s guard would
288
- # otherwise leave an orphan project item behind).
270
+ # Adds the issue to the pipeline at "Started". Skipped when the issue has pending approvals
271
+ # (Pipeline.take!'s guard would otherwise leave an orphan project item behind).
289
272
  #
290
273
  # @param issue [PlanMyStuff::Issue]
291
274
  #
@@ -297,7 +280,7 @@ module PlanMyStuff
297
280
  return
298
281
  end
299
282
 
300
- number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
283
+ number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
301
284
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
302
285
  PlanMyStuff::Pipeline.take!(project_item)
303
286
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- # Base class for all PMS gem jobs. Subclasses +::ActiveJob::Base+ so
5
- # the gem doesn't depend on the consuming app having an
6
- # +ApplicationJob+ constant.
4
+ # Base class for all PMS gem jobs. Subclasses +::ActiveJob::Base+ so the gem doesn't depend on the consuming app
5
+ # having an +ApplicationJob+ constant.
7
6
  class ApplicationJob < ::ActiveJob::Base
8
7
  end
9
8
  end