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,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
|
-
|
|
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
|