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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -1
- data/CONFIGURATION.md +351 -0
- 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 +3 -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 +52 -14
- 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 +62 -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 +59 -57
- data/lib/plan_my_stuff/comment_metadata.rb +1 -1
- data/lib/plan_my_stuff/configuration.rb +93 -93
- 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 +471 -333
- data/lib/plan_my_stuff/issue_metadata.rb +10 -10
- data/lib/plan_my_stuff/label.rb +34 -18
- 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 +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
|
-
|
|
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 =
|
|
14
|
-
|
|
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,
|
|
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 =
|
|
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?(
|
|
12
|
-
when 'regular' then all_projects.reject { |p| p.is_a?(
|
|
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 =
|
|
19
|
+
@project = PlanMyStuff::Project.new
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
# POST /projects
|
|
24
23
|
def create
|
|
25
|
-
@project =
|
|
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
|
|
35
|
-
|
|
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:
|
|
41
|
+
render(:new, status: PlanMyStuff.unprocessable_status)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# GET /projects/:id
|
|
45
45
|
def show
|
|
46
|
-
@project =
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
22
|
+
case params[:result]
|
|
23
|
+
when 'pass'
|
|
23
24
|
item.mark_passed!(pms_current_user)
|
|
24
25
|
flash[:success] = 'Item marked as Passed.'
|
|
25
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 =
|
|
56
|
+
project = PlanMyStuff::TestingProject.find(project_number)
|
|
47
57
|
item = project.items.find { |i| i.id == params[:item_id] }
|
|
48
|
-
raise(
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
25
|
-
|
|
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:
|
|
34
|
+
render(:new, status: PlanMyStuff.unprocessable_status)
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
# GET /testing_projects/:id
|
|
31
38
|
def show
|
|
32
|
-
@project =
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
63
|
-
rescue
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
133
|
-
#
|
|
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
|
-
#
|
|
138
|
-
# release
|
|
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.
|
|
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
|
-
#
|
|
174
|
-
#
|
|
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
|
-
#
|
|
179
|
-
#
|
|
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
|
-
#
|
|
236
|
-
#
|
|
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
|
-
#
|
|
258
|
-
# pipeline
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|