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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +569 -38
  4. data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
  5. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
  6. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
  7. data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
  9. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
  10. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
  11. data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
  12. data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
  13. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
  14. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
  15. data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
  16. data/app/controllers/plan_my_stuff/projects_controller.rb +65 -1
  17. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
  18. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
  19. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
  20. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
  21. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
  22. data/app/jobs/plan_my_stuff/application_job.rb +9 -0
  23. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
  24. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
  25. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
  26. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
  27. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
  28. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
  29. data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
  30. data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
  31. data/app/views/plan_my_stuff/projects/index.html.erb +17 -1
  32. data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
  33. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
  34. data/app/views/plan_my_stuff/projects/show.html.erb +11 -4
  35. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
  36. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
  37. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
  38. data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
  39. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
  40. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
  41. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
  42. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  43. data/config/routes.rb +38 -15
  44. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +138 -5
  45. data/lib/plan_my_stuff/application_record.rb +144 -0
  46. data/lib/plan_my_stuff/approval.rb +80 -0
  47. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  48. data/lib/plan_my_stuff/archive.rb +14 -0
  49. data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
  50. data/lib/plan_my_stuff/base_metadata.rb +0 -11
  51. data/lib/plan_my_stuff/base_project.rb +661 -0
  52. data/lib/plan_my_stuff/base_project_item.rb +562 -0
  53. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  54. data/lib/plan_my_stuff/cache.rb +197 -0
  55. data/lib/plan_my_stuff/client.rb +7 -0
  56. data/lib/plan_my_stuff/comment.rb +174 -54
  57. data/lib/plan_my_stuff/configuration.rb +254 -8
  58. data/lib/plan_my_stuff/custom_fields.rb +31 -17
  59. data/lib/plan_my_stuff/engine.rb +0 -4
  60. data/lib/plan_my_stuff/errors.rb +49 -0
  61. data/lib/plan_my_stuff/graphql/queries.rb +392 -0
  62. data/lib/plan_my_stuff/issue.rb +1477 -174
  63. data/lib/plan_my_stuff/issue_metadata.rb +122 -0
  64. data/lib/plan_my_stuff/label.rb +82 -11
  65. data/lib/plan_my_stuff/link.rb +144 -0
  66. data/lib/plan_my_stuff/notifications.rb +142 -0
  67. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  68. data/lib/plan_my_stuff/pipeline/status.rb +44 -0
  69. data/lib/plan_my_stuff/pipeline.rb +293 -0
  70. data/lib/plan_my_stuff/project.rb +62 -468
  71. data/lib/plan_my_stuff/project_item.rb +3 -417
  72. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  73. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  74. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  75. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  76. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  77. data/lib/plan_my_stuff/reminders.rb +16 -0
  78. data/lib/plan_my_stuff/test_helpers.rb +260 -15
  79. data/lib/plan_my_stuff/testing_project.rb +291 -0
  80. data/lib/plan_my_stuff/testing_project_item.rb +184 -0
  81. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  82. data/lib/plan_my_stuff/user_resolver.rb +8 -3
  83. data/lib/plan_my_stuff/version.rb +1 -1
  84. data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
  85. data/lib/plan_my_stuff.rb +16 -0
  86. data/lib/tasks/plan_my_stuff.rake +163 -0
  87. metadata +54 -2
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module ProjectItems
5
+ # Handles moving a project item to a new status column via CRUD-style routes.
6
+ # Backs the status dropdown on the project board view (T-046).
7
+ #
8
+ # PATCH /projects/:project_id/items/:item_id/status -> update (moves to new status)
9
+ #
10
+ class StatusesController < ApplicationController
11
+ # PATCH /projects/:project_id/items/:item_id/status
12
+ def update
13
+ item = find_project_item
14
+
15
+ item.move_to!(params[:status])
16
+
17
+ flash[:success] = "Item moved to #{params[:status]}."
18
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
19
+ rescue ArgumentError, PMS::Error => e
20
+ flash[:error] = e.message
21
+ redirect_to(plan_my_stuff.project_path(params[:project_id]))
22
+ end
23
+
24
+ private
25
+
26
+ # Finds the project item by item_id within the given project.
27
+ #
28
+ # @return [PlanMyStuff::ProjectItem]
29
+ #
30
+ def find_project_item
31
+ project = PMS::Project.find(params[:project_id].to_i)
32
+ item = project.items.find { |i| i.id == params[:item_id] }
33
+
34
+ raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
35
+
36
+ item
37
+ end
38
+ end
39
+ end
40
+ end
@@ -20,80 +20,5 @@ module PlanMyStuff
20
20
  flash[:error] = e.message
21
21
  redirect_to(plan_my_stuff.project_path(project_number))
22
22
  end
23
-
24
- # PATCH /projects/:project_id/items/:id/move
25
- def move
26
- item = find_project_item
27
-
28
- item.move_to!(params[:status])
29
-
30
- flash[:success] = "Item moved to #{params[:status]}."
31
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
32
- rescue ArgumentError, PMS::Error => e
33
- flash[:error] = e.message
34
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
35
- end
36
-
37
- # PATCH /projects/:project_id/items/:id/assign
38
- def assign
39
- item = find_project_item
40
- assignees = parse_assignees(params[:assignee])
41
-
42
- item.assign!(assignees)
43
-
44
- flash[:success] = "Item assigned to #{assignees.join(', ')}."
45
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
46
- rescue ArgumentError, PMS::Error => e
47
- flash[:error] = e.message
48
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
49
- end
50
-
51
- # PATCH /projects/:project_id/items/:id/unassign
52
- def unassign
53
- if params[:username].blank?
54
- flash[:error] = 'Username is required to unassign.'
55
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
56
- return
57
- end
58
-
59
- item = find_project_item
60
- current_assignees = item.field_values['Assignees'] || []
61
- remaining = current_assignees - [params[:username]]
62
-
63
- item.assign!(remaining)
64
-
65
- flash[:success] = "#{params[:username]} unassigned."
66
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
67
- rescue ArgumentError, PMS::Error => e
68
- flash[:error] = e.message
69
- redirect_to(plan_my_stuff.project_path(params[:project_id]))
70
- end
71
-
72
- private
73
-
74
- # Finds the project and the item within it.
75
- #
76
- # @return [PlanMyStuff::ProjectItem]
77
- #
78
- def find_project_item
79
- project = PMS::Project.find(params[:project_id].to_i)
80
- item = project.items.find { |i| i.id == params[:id] }
81
-
82
- raise(PMS::APIError, "Item not found: #{params[:id]}") unless item
83
-
84
- item
85
- end
86
-
87
- # Splits a comma-separated assignees string into an array.
88
- #
89
- # @param assignees_string [String, nil]
90
- #
91
- # @return [Array<String>]
92
- #
93
- def parse_assignees(assignees_string)
94
- return [] if assignees_string.blank?
95
-
96
- assignees_string.split(',').filter_map { |a| a.strip.presence }
97
- end
98
23
  end
99
24
  end
@@ -4,7 +4,41 @@ module PlanMyStuff
4
4
  class ProjectsController < ApplicationController
5
5
  # GET /projects
6
6
  def index
7
- @projects = PMS::Project.list.reject(&:closed)
7
+ all_projects = PMS::BaseProject.list.reject(&:closed)
8
+ @filter = params[:filter].presence_in(%w[testing all]) || 'regular'
9
+ @projects =
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) }
13
+ else all_projects
14
+ end
15
+ @support_user = support_user?
16
+ end
17
+
18
+ # GET /projects/new
19
+ def new
20
+ @project = PMS::Project.new
21
+ end
22
+
23
+ # POST /projects
24
+ def create
25
+ @project = PMS::Project.create!(
26
+ title: project_params[:title],
27
+ readme: project_params[:readme] || '',
28
+ description: project_params[:description],
29
+ user: pms_current_user,
30
+ )
31
+
32
+ flash[:success] = 'Project was successfully created.'
33
+ redirect_to(plan_my_stuff.project_path(@project.number))
34
+ rescue PMS::ValidationError => e
35
+ @project = PMS::Project.new(
36
+ title: project_params[:title],
37
+ readme: project_params[:readme],
38
+ description: project_params[:description],
39
+ )
40
+ flash.now[:error] = e.message
41
+ render(:new, status: PMS.unprocessable_status)
8
42
  end
9
43
 
10
44
  # GET /projects/:id
@@ -12,6 +46,36 @@ module PlanMyStuff
12
46
  @project = PMS::Project.find(params[:id].to_i)
13
47
  @statuses = @project.statuses.pluck(:name)
14
48
  @items_by_status = @project.items.group_by(&:status)
49
+ @support_user = support_user?
50
+ end
51
+
52
+ # GET /projects/:id/edit
53
+ def edit
54
+ @project = PMS::Project.find(params[:id].to_i)
15
55
  end
56
+
57
+ # PATCH/PUT /projects/:id
58
+ def update
59
+ @project = PMS::Project.find(params[:id].to_i)
60
+
61
+ @project.update!(
62
+ title: project_params[:title],
63
+ readme: project_params[:readme],
64
+ description: project_params[:description],
65
+ )
66
+
67
+ flash[:success] = 'Project was successfully updated.'
68
+ redirect_to(plan_my_stuff.project_path(@project.number))
69
+ rescue PMS::StaleObjectError
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)
72
+ end
73
+
74
+ private
75
+
76
+ # @return [ActionController::Parameters]
77
+ def project_params
78
+ params.require(:project).permit(:title, :readme, :description)
79
+ end
16
80
  end
17
81
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module TestingProjectItems
5
+ # Handles Pass/Fail outcomes for testing project items.
6
+ #
7
+ # Pass is submitted directly via button_to with result: "pass".
8
+ # Fail renders a form (new) to capture result_notes before posting to create.
9
+ #
10
+ class ResultsController < ApplicationController
11
+ # GET /testing_projects/:testing_project_id/items/:item_id/result/new
12
+ def new
13
+ @project_number = params[:testing_project_id].to_i
14
+ @item_id = params[:item_id]
15
+ end
16
+
17
+ # POST /testing_projects/:testing_project_id/items/:item_id/result
18
+ def create
19
+ project_number = params[:testing_project_id].to_i
20
+ item = find_project_item(project_number)
21
+
22
+ if params[:result] == 'pass'
23
+ item.mark_passed!(pms_current_user)
24
+ flash[:success] = 'Item marked as Passed.'
25
+ else
26
+ item.mark_failed!(pms_current_user, result_notes: params[:result_notes])
27
+ flash[:success] = 'Item marked as Failed.'
28
+ end
29
+
30
+ 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
37
+ flash[:error] = e.message
38
+ redirect_to(plan_my_stuff.testing_project_path(params[:testing_project_id].to_i))
39
+ end
40
+
41
+ private
42
+
43
+ # @param project_number [Integer]
44
+ # @return [PlanMyStuff::TestingProjectItem]
45
+ def find_project_item(project_number)
46
+ project = PMS::TestingProject.find(project_number)
47
+ item = project.items.find { |i| i.id == params[:item_id] }
48
+ raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
49
+
50
+ item
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ class TestingProjectItemsController < ApplicationController
5
+ # GET /testing_projects/:testing_project_id/items/new
6
+ def new
7
+ @project = PMS::TestingProject.find(params[:testing_project_id].to_i)
8
+ end
9
+
10
+ # POST /testing_projects/:testing_project_id/items
11
+ def create
12
+ project_number = params[:testing_project_id].to_i
13
+ item = PMS::TestingProjectItem.create!(
14
+ item_params[:title],
15
+ draft: true,
16
+ body: item_params[:body],
17
+ project_number: project_number,
18
+ )
19
+
20
+ item.update_testers!(item_params[:testers]) if item_params[:testers].present?
21
+ item.update_watchers!(item_params[:watchers]) if item_params[:watchers].present?
22
+ item.update_due_date!(Date.parse(item_params[:due_date])) if item_params[:due_date].present?
23
+ item.update_pass_mode!(item_params[:pass_mode]) if item_params[:pass_mode].present?
24
+
25
+ flash[:success] = 'Item added.'
26
+ redirect_to(plan_my_stuff.testing_project_path(project_number))
27
+ rescue ArgumentError, PMS::Error, Octokit::Error => e
28
+ flash[:error] = e.message
29
+ redirect_to(plan_my_stuff.testing_project_path(project_number))
30
+ end
31
+
32
+ private
33
+
34
+ # @return [ActionController::Parameters]
35
+ def item_params
36
+ params.permit(:title, :body, :testers, :watchers, :due_date, :pass_mode)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ class TestingProjectsController < ApplicationController
5
+ # GET /testing_projects/new
6
+ def new
7
+ @project = PMS::TestingProject.new
8
+ @project.metadata.subject_urls = [params[:subject_url]] if params[:subject_url].present?
9
+ end
10
+
11
+ # POST /testing_projects
12
+ def create
13
+ @project = PMS::TestingProject.create!(
14
+ title: testing_project_params[:title],
15
+ description: testing_project_params[:description],
16
+ subject_urls: parse_subject_urls(testing_project_params[:subject_urls]),
17
+ due_date: parse_due_date(testing_project_params[:due_date]),
18
+ deadline_miss_reason: testing_project_params[:deadline_miss_reason],
19
+ user: pms_current_user,
20
+ )
21
+
22
+ flash[:success] = 'Testing project was successfully created.'
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])
26
+ flash.now[:error] = e.message
27
+ render(:new, status: PMS.unprocessable_status)
28
+ end
29
+
30
+ # GET /testing_projects/:id
31
+ def show
32
+ @project = PMS::TestingProject.find(params[:id].to_i)
33
+ @statuses = @project.statuses.pluck(:name)
34
+ @items_by_status = @project.items.group_by(&:status)
35
+ @support_user = support_user?
36
+ end
37
+
38
+ # GET /testing_projects/:id/edit
39
+ def edit
40
+ @project = PMS::TestingProject.find(params[:id].to_i)
41
+ end
42
+
43
+ # PATCH/PUT /testing_projects/:id
44
+ def update
45
+ @project = PMS::TestingProject.find(params[:id].to_i)
46
+
47
+ @project.update!(
48
+ title: testing_project_params[:title],
49
+ description: testing_project_params[:description],
50
+ metadata: {
51
+ subject_urls: parse_subject_urls(testing_project_params[:subject_urls]),
52
+ due_date: parse_due_date(testing_project_params[:due_date]),
53
+ deadline_miss_reason: testing_project_params[:deadline_miss_reason],
54
+ },
55
+ )
56
+
57
+ flash[:success] = 'Testing project was successfully updated.'
58
+ redirect_to(plan_my_stuff.testing_project_path(@project.number))
59
+ rescue PMS::StaleObjectError
60
+ flash.now[:error] =
61
+ '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
64
+ flash.now[:error] = e.message
65
+ render(:edit, status: PMS.unprocessable_status)
66
+ end
67
+
68
+ private
69
+
70
+ # @param raw [String, nil]
71
+ # @return [Array<String>]
72
+ def parse_subject_urls(raw)
73
+ raw.to_s.split("\n").map(&:strip).compact_blank
74
+ end
75
+
76
+ # @param raw [String, nil]
77
+ # @return [Date, nil]
78
+ def parse_due_date(raw)
79
+ return if raw.blank?
80
+
81
+ Date.parse(raw)
82
+ rescue ArgumentError, TypeError
83
+ nil
84
+ end
85
+
86
+ # @return [ActionController::Parameters]
87
+ def testing_project_params
88
+ params.require(:testing_project).permit(
89
+ :title, :description, :due_date, :deadline_miss_reason, :subject_urls,
90
+ )
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module Webhooks
5
+ class AwsController < ActionController::API
6
+ VALID_SNS_MESSAGE_TYPES = %w[Notification].freeze
7
+
8
+ before_action :verify_signature!
9
+
10
+ # POST /webhooks/aws
11
+ def create
12
+ config = PlanMyStuff.configuration
13
+
14
+ unless config.process_aws_webhooks
15
+ head(:ok)
16
+
17
+ return
18
+ end
19
+
20
+ unless matching_service?(config.aws_service_identifier)
21
+ head(:ok)
22
+
23
+ return
24
+ end
25
+
26
+ case sns_message_params.dig(:detail, :event_name)
27
+ when 'SERVICE_DEPLOYMENT_COMPLETED'
28
+ handle_deployment_completed
29
+ end
30
+
31
+ head(:ok)
32
+ end
33
+
34
+ private
35
+
36
+ # Validates the SNS message signature.
37
+ # Checks message type header and topic ARN first (cheap), then
38
+ # delegates to the configured verifier class for cryptographic
39
+ # certificate-based verification.
40
+ #
41
+ # @return [void]
42
+ #
43
+ def verify_signature!
44
+ message_type = request.headers['x-amz-sns-message-type'].to_s
45
+
46
+ if VALID_SNS_MESSAGE_TYPES.exclude?(message_type)
47
+ head(:unauthorized)
48
+
49
+ return
50
+ end
51
+
52
+ config = PlanMyStuff.configuration
53
+
54
+ unless sns_params[:topic_arn] == config.sns_topic_arn
55
+ head(:unauthorized)
56
+
57
+ return
58
+ end
59
+
60
+ verifier_error = config.sns_verifier_error
61
+ config.sns_verifier_class.new.authenticate!(raw_body)
62
+ rescue verifier_error
63
+ head(:unauthorized)
64
+ end
65
+
66
+ # @return [String]
67
+ def raw_body
68
+ @raw_body ||=
69
+ begin
70
+ body = request.body.read
71
+ request.body.rewind
72
+ body
73
+ end
74
+ end
75
+
76
+ # Parses the SNS envelope. Returns empty params if the body is
77
+ # not valid JSON so signature verification rejects the request
78
+ # with 401 instead of bubbling a 500.
79
+ #
80
+ # @return [ActionController::Parameters]
81
+ #
82
+ def sns_params
83
+ @sns_params ||= ActionController::Parameters.new(JSON.parse(raw_body).deep_transform_keys(&:underscore))
84
+ rescue JSON::ParserError
85
+ @sns_params ||= ActionController::Parameters.new
86
+ end
87
+
88
+ # Parses the nested SNS message payload. Returns empty params on
89
+ # malformed JSON so callers degrade gracefully.
90
+ #
91
+ # @return [ActionController::Parameters]
92
+ #
93
+ def sns_message_params
94
+ parsed = JSON.parse(sns_params.fetch(:message, '{}')).deep_transform_keys(&:underscore)
95
+ ActionController::Parameters.new(parsed)
96
+ rescue JSON::ParserError
97
+ ActionController::Parameters.new
98
+ end
99
+
100
+ # Checks whether any resource ARN in the ECS event matches the
101
+ # configured service identifier suffix.
102
+ #
103
+ # @param service_identifier [String, nil]
104
+ #
105
+ # @return [Boolean]
106
+ #
107
+ def matching_service?(service_identifier)
108
+ return false if service_identifier.blank?
109
+
110
+ sns_message_params.fetch(:resources, []).any? { |arn| arn.end_with?(service_identifier) }
111
+ end
112
+
113
+ # Finds "Release in Progress" items whose linked issue commit SHA
114
+ # matches the configured production commit SHA (prefix match),
115
+ # then completes deployment for each.
116
+ #
117
+ # @return [void]
118
+ #
119
+ def handle_deployment_completed
120
+ sha = PlanMyStuff.configuration.production_commit_sha
121
+ raise(PlanMyStuff::PipelineError, 'production_commit_sha is not configured') if sha.nil?
122
+
123
+ release_in_progress_items.each do |item|
124
+ next if item.draft?
125
+
126
+ next unless item.issue.metadata.commit_sha&.start_with?(sha)
127
+
128
+ PlanMyStuff::Pipeline.complete_deployment!(item)
129
+ end
130
+ end
131
+
132
+ # Finds all pipeline project items at "Release in Progress" status.
133
+ #
134
+ # @return [Array<PlanMyStuff::ProjectItem>]
135
+ #
136
+ def release_in_progress_items
137
+ project_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
138
+ project = PlanMyStuff::Project.find(project_number)
139
+
140
+ release_status = PlanMyStuff::Pipeline.resolve_status_name(
141
+ PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS,
142
+ )
143
+
144
+ project.items.select { |item| item.status == release_status }
145
+ end
146
+ end
147
+ end
148
+ end