plan_my_stuff 0.3.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 +11 -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/index.html.erb +15 -1
- data/app/views/plan_my_stuff/projects/show.html.erb +10 -5
- 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 +121 -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_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 +171 -50
- data/lib/plan_my_stuff/configuration.rb +210 -10
- 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 +1476 -175
- 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 +30 -693
- 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 +9 -3
- 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 +15 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +50 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<h1><%= @project.title %></h1>
|
|
2
2
|
|
|
3
|
-
<p
|
|
3
|
+
<p>
|
|
4
|
+
<%= link_to('Edit', plan_my_stuff.edit_project_path(@project.number)) %>
|
|
5
|
+
<% if @support_user && @project.url.present? %>
|
|
6
|
+
<%= link_to('View on GitHub', @project.url, target: '_blank', rel: 'noopener') %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</p>
|
|
4
9
|
|
|
5
10
|
<% if @statuses.any? %>
|
|
6
11
|
<table>
|
|
@@ -29,7 +34,7 @@
|
|
|
29
34
|
|
|
30
35
|
<%=
|
|
31
36
|
form_with(
|
|
32
|
-
url: plan_my_stuff.
|
|
37
|
+
url: plan_my_stuff.project_item_status_path(@project.number, item.id),
|
|
33
38
|
method: :patch,
|
|
34
39
|
local: true,
|
|
35
40
|
) do |form|
|
|
@@ -45,8 +50,8 @@
|
|
|
45
50
|
<%= username %>
|
|
46
51
|
<%=
|
|
47
52
|
form_with(
|
|
48
|
-
url: plan_my_stuff.
|
|
49
|
-
method: :
|
|
53
|
+
url: plan_my_stuff.project_item_assignment_path(@project.number, item.id),
|
|
54
|
+
method: :delete,
|
|
50
55
|
local: true,
|
|
51
56
|
html: { style: 'display: inline' },
|
|
52
57
|
) do |form|
|
|
@@ -61,7 +66,7 @@
|
|
|
61
66
|
|
|
62
67
|
<%=
|
|
63
68
|
form_with(
|
|
64
|
-
url: plan_my_stuff.
|
|
69
|
+
url: plan_my_stuff.project_item_assignment_path(@project.number, item.id),
|
|
65
70
|
method: :patch,
|
|
66
71
|
local: true,
|
|
67
72
|
) do |form|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<h1>Add Item to <%= @project.title %></h1>
|
|
2
|
+
|
|
3
|
+
<% if flash[:error].present? %>
|
|
4
|
+
<p style="color: red;"><%= flash[:error] %></p>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<%=
|
|
8
|
+
render({
|
|
9
|
+
partial: 'plan_my_stuff/testing_projects/partials/items/form',
|
|
10
|
+
locals: { project: @project },
|
|
11
|
+
})
|
|
12
|
+
%>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<h1>Fail Item</h1>
|
|
2
|
+
|
|
3
|
+
<% if flash[:error].present? %>
|
|
4
|
+
<p style="color: red;"><%= flash[:error] %></p>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<%= form_with(
|
|
8
|
+
url: plan_my_stuff.testing_project_item_result_path(@project_number, @item_id),
|
|
9
|
+
method: :post,
|
|
10
|
+
local: true,
|
|
11
|
+
) do |form| %>
|
|
12
|
+
<%= form.hidden_field(:result, value: 'fail') %>
|
|
13
|
+
|
|
14
|
+
<div>
|
|
15
|
+
<%= form.label(:result_notes, 'Result Notes') %>
|
|
16
|
+
<%= form.text_area(:result_notes, rows: 5, required: true) %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div>
|
|
20
|
+
<%= form.submit('Submit Failure') %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%
|
|
2
|
+
persisted = project.persisted?
|
|
3
|
+
url =
|
|
4
|
+
if persisted
|
|
5
|
+
plan_my_stuff.testing_project_path(project.number)
|
|
6
|
+
else
|
|
7
|
+
plan_my_stuff.testing_projects_path
|
|
8
|
+
end
|
|
9
|
+
%>
|
|
10
|
+
<%= form_with(url: url, method: persisted ? :patch : :post, scope: :testing_project) do |form| %>
|
|
11
|
+
<div>
|
|
12
|
+
<%= form.label(:title, 'Title') %>
|
|
13
|
+
<%= form.text_field(:title, value: project.title, required: true) %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div>
|
|
17
|
+
<%= form.label(:description, 'Description') %>
|
|
18
|
+
<%= form.text_field(:description, value: project.description) %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div>
|
|
22
|
+
<%= form.label(:subject_urls, 'Subject URLs (one per line)') %>
|
|
23
|
+
<%= form.text_area(:subject_urls, rows: 4, value: project.metadata.subject_urls.join("\n")) %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<%= form.label(:due_date, 'Due Date') %>
|
|
28
|
+
<%= form.date_field(:due_date, value: project.metadata.due_date) %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<%= form.label(:deadline_miss_reason, 'Deadline Miss Reason') %>
|
|
33
|
+
<%= form.text_field(:deadline_miss_reason, value: project.metadata.deadline_miss_reason) %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<%= form.submit(persisted ? 'Update Testing Project' : 'Create Testing Project') %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div style="border: 1px solid black; margin: 1em; padding: 0.5em">
|
|
2
|
+
<% if item.draft? %>
|
|
3
|
+
<strong><%= item.title %></strong>
|
|
4
|
+
<% else %>
|
|
5
|
+
<strong><%= link_to(item.title, plan_my_stuff.issue_path(item.number, repo: item.repo.full_name)) %></strong>
|
|
6
|
+
<small>#<%= item.number %></small>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%=
|
|
10
|
+
form_with(
|
|
11
|
+
url: plan_my_stuff.testing_project_item_status_path(project.number, item.id),
|
|
12
|
+
method: :patch,
|
|
13
|
+
local: true,
|
|
14
|
+
) do |form|
|
|
15
|
+
%>
|
|
16
|
+
<%= form.select(:status, statuses, { selected: item.status }, { onchange: 'this.form.requestSubmit();' }) %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if item.field_values['Testers'].present? %>
|
|
20
|
+
<div><strong>Testers:</strong> <%= item.field_values['Testers'] %></div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% if item.field_values['Watchers'].present? %>
|
|
24
|
+
<div><strong>Watchers:</strong> <%= item.field_values['Watchers'] %></div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<% if item.field_values['Due Date'].present? %>
|
|
28
|
+
<div><strong>Due:</strong> <%= item.field_values['Due Date'] %></div>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% if item.field_values['Result Notes'].present? %>
|
|
32
|
+
<div><strong>Notes:</strong> <%= item.field_values['Result Notes'] %></div>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<% if item.field_values['Passed At'].present? %>
|
|
36
|
+
<div><strong>Passed at:</strong> <%= item.field_values['Passed At'] %></div>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<%=
|
|
41
|
+
button_to(
|
|
42
|
+
'Pass',
|
|
43
|
+
plan_my_stuff.testing_project_item_result_path(project.number, item.id),
|
|
44
|
+
method: :post,
|
|
45
|
+
params: { result: 'pass' },
|
|
46
|
+
)
|
|
47
|
+
%>
|
|
48
|
+
|
|
49
|
+
<%= link_to('Fail', plan_my_stuff.new_testing_project_item_result_path(project.number, item.id)) %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%= form_with(url: plan_my_stuff.testing_project_items_path(project.number), method: :post, local: true) do |form| %>
|
|
2
|
+
<div>
|
|
3
|
+
<%= form.label(:title, 'Title') %>
|
|
4
|
+
<%= form.text_field(:title, required: true) %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div>
|
|
8
|
+
<%= form.label(:body, 'Body') %>
|
|
9
|
+
<%= form.text_area(:body, rows: 4) %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div>
|
|
13
|
+
<%= form.label(:testers, 'Testers (user ids, comma separated)') %>
|
|
14
|
+
<%= form.text_field(:testers) %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div>
|
|
18
|
+
<%= form.label(:watchers, 'Watchers (user ids, comma separated)') %>
|
|
19
|
+
<%= form.text_field(:watchers) %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div>
|
|
23
|
+
<%= form.label(:pass_mode, 'Pass Mode') %>
|
|
24
|
+
<%= form.select(:pass_mode, [['All testers must pass', 'all'], ['Any tester can pass', 'any']], { selected: 'all' }) %>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<%= form.label(:due_date, 'Due Date') %>
|
|
29
|
+
<%= form.date_field(:due_date) %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div>
|
|
33
|
+
<%= form.submit('Add Item') %>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<h1><%= @project.title %></h1>
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<%= link_to('Edit', plan_my_stuff.edit_testing_project_path(@project.number)) %>
|
|
5
|
+
<% if @support_user && @project.url.present? %>
|
|
6
|
+
<%= link_to('View on GitHub', @project.url, target: '_blank', rel: 'noopener') %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<% if @support_user && @project.metadata.subject_urls.any? %>
|
|
11
|
+
<p>
|
|
12
|
+
<strong>Subject URLs:</strong>
|
|
13
|
+
<% @project.metadata.subject_urls.each do |url| %>
|
|
14
|
+
<%= link_to(url, url, target: '_blank', rel: 'noopener') %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</p>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if @project.metadata.due_date.present? %>
|
|
20
|
+
<p><strong>Due:</strong> <%= @project.metadata.due_date %></p>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% if @statuses.any? %>
|
|
24
|
+
<table>
|
|
25
|
+
<thead>
|
|
26
|
+
<tr>
|
|
27
|
+
<% @statuses.each do |status| %>
|
|
28
|
+
<th><%= status %></th>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<tr>
|
|
34
|
+
<% @statuses.each do |status| %>
|
|
35
|
+
<td>
|
|
36
|
+
<% items = @items_by_status[status] || [] %>
|
|
37
|
+
<% items.each do |item| %>
|
|
38
|
+
<%= render({
|
|
39
|
+
partial: 'plan_my_stuff/testing_projects/partials/item',
|
|
40
|
+
locals: { item: item, project: @project, statuses: @statuses },
|
|
41
|
+
}) %>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% if items.empty? %>
|
|
44
|
+
<em>No items</em>
|
|
45
|
+
<% end %>
|
|
46
|
+
</td>
|
|
47
|
+
<% end %>
|
|
48
|
+
</tr>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
<% else %>
|
|
52
|
+
<p>No statuses configured for this project.</p>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<h2>Add Item</h2>
|
|
56
|
+
|
|
57
|
+
<p><%= link_to('Add item with details', plan_my_stuff.new_testing_project_item_path(@project.number)) %></p>
|
|
58
|
+
|
|
59
|
+
<%= form_with(url: plan_my_stuff.testing_project_items_path(@project.number), method: :post, local: true) do |form| %>
|
|
60
|
+
<%= form.label(:title, 'Title') %>
|
|
61
|
+
<%= form.text_field(:title, required: true) %>
|
|
62
|
+
<%= form.label(:body, 'Body') %>
|
|
63
|
+
<%= form.text_area(:body) %>
|
|
64
|
+
<%= form.submit('Add Item') %>
|
|
65
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
PlanMyStuff::Engine.routes.draw do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
config = PlanMyStuff.configuration
|
|
5
|
+
mount_groups = config.mount_groups
|
|
6
|
+
|
|
7
|
+
if mount_groups.fetch(:issues, true)
|
|
8
|
+
resources :issues, except: %i[destroy], controller: config.controller_for(:issues) do
|
|
9
|
+
resource :closure, only: %i[create destroy], controller: config.controller_for(:'issues/closures')
|
|
10
|
+
resource :waiting, only: %i[create destroy], controller: config.controller_for(:'issues/waitings')
|
|
11
|
+
resources :viewers, only: %i[create destroy], controller: config.controller_for(:'issues/viewers')
|
|
12
|
+
if config.pipeline_enabled
|
|
13
|
+
resource(
|
|
14
|
+
:take,
|
|
15
|
+
only: :create,
|
|
16
|
+
controller: config.controller_for(:'issues/takes'),
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
resources :comments, only: %i[create edit update], controller: config.controller_for(:comments)
|
|
20
|
+
resources :labels, only: %i[create destroy], controller: config.controller_for(:labels)
|
|
21
|
+
resources :links, only: %i[create destroy], controller: config.controller_for(:'issues/links')
|
|
22
|
+
resources :approvals, only: %i[create update destroy], controller: config.controller_for(:'issues/approvals')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if mount_groups.fetch(:webhooks, true) && config.pipeline_enabled
|
|
27
|
+
namespace :webhooks do
|
|
28
|
+
resource :github, only: %i[create], controller: config.controller_for(:'webhooks/github')
|
|
29
|
+
resource :aws, only: %i[create], controller: config.controller_for(:'webhooks/aws')
|
|
10
30
|
end
|
|
11
|
-
resources :comments, only: %i[create edit update]
|
|
12
|
-
post 'labels', to: 'labels#add_to_issue', as: :add_label
|
|
13
|
-
delete 'labels/:name', to: 'labels#remove_from_issue', as: :remove_label
|
|
14
31
|
end
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
resources :
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
if mount_groups.fetch(:projects, true)
|
|
34
|
+
resources :projects, except: %i[destroy], controller: config.controller_for(:projects) do
|
|
35
|
+
resources :items, only: %i[create], controller: config.controller_for(:project_items) do
|
|
36
|
+
resource :status, only: %i[update], controller: config.controller_for(:'project_items/statuses')
|
|
37
|
+
resource :assignment, only: %i[update destroy], controller: config.controller_for(:'project_items/assignments')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
resources :testing_projects, except: %i[destroy index], controller: config.controller_for(:testing_projects) do
|
|
42
|
+
resources :items, only: %i[new create], controller: config.controller_for(:testing_project_items) do
|
|
43
|
+
resource :status, only: %i[update], controller: config.controller_for(:'project_items/statuses')
|
|
44
|
+
resource :result, only: %i[new create], controller: config.controller_for(:'testing_project_items/results')
|
|
22
45
|
end
|
|
23
46
|
end
|
|
24
47
|
end
|
|
@@ -26,6 +26,13 @@ PMS.configure do |config|
|
|
|
26
26
|
# Default GitHub Projects V2 number for add_to_project calls.
|
|
27
27
|
# config.default_project_number = 14
|
|
28
28
|
|
|
29
|
+
# Template project to clone when creating new Testing Projects.
|
|
30
|
+
# When set, TestingProject.create! copies this project (preserving its
|
|
31
|
+
# custom fields and board layout) instead of bootstrapping fields from scratch.
|
|
32
|
+
# The template should be a pre-configured testing project with the correct
|
|
33
|
+
# fields and board view already set up in the GitHub UI.
|
|
34
|
+
# config.testing_template_project_number = 42
|
|
35
|
+
|
|
29
36
|
# --------------------------------------------------------------------------
|
|
30
37
|
# App identity
|
|
31
38
|
# --------------------------------------------------------------------------
|
|
@@ -85,11 +92,9 @@ PMS.configure do |config|
|
|
|
85
92
|
# update_status: 'PmsUpdateStatusJob'
|
|
86
93
|
# }
|
|
87
94
|
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
# config.
|
|
91
|
-
# config.deferred_email_from = 'noreply@example.com'
|
|
92
|
-
# config.deferred_email_to = 'it-support@example.com'
|
|
95
|
+
# Fallback actor for notification events (plan_my_stuff.*) when a caller
|
|
96
|
+
# does not pass an explicit user: kwarg. Proc/lambda called at event time.
|
|
97
|
+
# config.current_user = -> { Current.user }
|
|
93
98
|
|
|
94
99
|
# --------------------------------------------------------------------------
|
|
95
100
|
# Custom fields
|
|
@@ -112,4 +117,132 @@ PMS.configure do |config|
|
|
|
112
117
|
|
|
113
118
|
# Comment-only fields (merged on top of shared, context wins on conflicts):
|
|
114
119
|
# config.comment_custom_fields = {}
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------------
|
|
122
|
+
# Release pipeline
|
|
123
|
+
# --------------------------------------------------------------------------
|
|
124
|
+
# config.pipeline_enabled = true
|
|
125
|
+
# config.pipeline_project_number = 14
|
|
126
|
+
|
|
127
|
+
# GitHub webhook HMAC-SHA256 secret (required when webhook routes are mounted).
|
|
128
|
+
# config.webhook_secret = Rails.application.credentials.dig(:plan_my_stuff, :webhook_secret)
|
|
129
|
+
|
|
130
|
+
# Configurable display aliases for pipeline statuses.
|
|
131
|
+
# config.pipeline_statuses = {
|
|
132
|
+
# 'Submitted' => 'Triaged',
|
|
133
|
+
# 'Completed' => 'Done',
|
|
134
|
+
# }
|
|
135
|
+
|
|
136
|
+
# config.main_branch = 'main'
|
|
137
|
+
# config.production_branch = 'production'
|
|
138
|
+
|
|
139
|
+
# --------------------------------------------------------------------------
|
|
140
|
+
# Follow-up reminders
|
|
141
|
+
# --------------------------------------------------------------------------
|
|
142
|
+
# Daily sweep that fires `plan_my_stuff.issue.reminder_due` events on
|
|
143
|
+
# waiting issues and auto-closes issues that exceed the inactivity
|
|
144
|
+
# ceiling. Consuming app is responsible for the initial enqueue:
|
|
145
|
+
#
|
|
146
|
+
# # Enqueue one sweep job per configured repo:
|
|
147
|
+
# rake plan_my_stuff:reminders:sweep
|
|
148
|
+
#
|
|
149
|
+
# # Or a single repo:
|
|
150
|
+
# rake plan_my_stuff:reminders:sweep REPO=element
|
|
151
|
+
#
|
|
152
|
+
# Each job self-requeues after perform (default: 6:30am ET next day);
|
|
153
|
+
# running the rake task once on app boot is enough. Override
|
|
154
|
+
# `RemindersSweepJob.next_run` on a subclass to change the cadence.
|
|
155
|
+
#
|
|
156
|
+
# config.reminders_enabled = true
|
|
157
|
+
#
|
|
158
|
+
# Days-since-waiting at which reminders fire. Per-issue override via
|
|
159
|
+
# `issue.metadata.reminder_days = [...]`.
|
|
160
|
+
# config.reminder_days = [1, 3, 7, 10, 14, 18]
|
|
161
|
+
#
|
|
162
|
+
# Days of continuous waiting before the sweep auto-closes the issue
|
|
163
|
+
# (emits `issue.closed_inactive` instead of the regular `issue.closed`).
|
|
164
|
+
# config.inactivity_close_days = 30
|
|
165
|
+
#
|
|
166
|
+
# Label names the gem creates and manages on waiting/inactive issues.
|
|
167
|
+
# config.waiting_on_user_label = 'waiting-on-user'
|
|
168
|
+
# config.waiting_on_approval_label = 'waiting-on-approval'
|
|
169
|
+
# config.user_inactive_label = 'user-inactive'
|
|
170
|
+
|
|
171
|
+
# --------------------------------------------------------------------------
|
|
172
|
+
# Auto-archiving of aged-closed issues
|
|
173
|
+
# --------------------------------------------------------------------------
|
|
174
|
+
# Piggybacks the RemindersSweepJob. For every issue closed more than
|
|
175
|
+
# `archive_closed_after_days` ago (based on GitHub's `closed_at`) the
|
|
176
|
+
# sweep: adds the configured label, locks the conversation, removes
|
|
177
|
+
# the issue from every Projects V2 board, stamps `metadata.archived_at`,
|
|
178
|
+
# and emits `plan_my_stuff.issue.archived` with `reason: :aged_closed`.
|
|
179
|
+
# Issues auto-closed by the inactivity sweep (closed_by_inactivity) are
|
|
180
|
+
# excluded. Non-PMS issues are excluded.
|
|
181
|
+
#
|
|
182
|
+
# config.archiving_enabled = true
|
|
183
|
+
# config.archive_closed_after_days = 90
|
|
184
|
+
# config.archived_label = 'archived'
|
|
185
|
+
|
|
186
|
+
# --------------------------------------------------------------------------
|
|
187
|
+
# AWS webhook (ECS deployments via SNS)
|
|
188
|
+
# --------------------------------------------------------------------------
|
|
189
|
+
# Expected SNS topic ARN for webhook validation.
|
|
190
|
+
# config.sns_topic_arn = 'arn:aws:sns:us-east-1:123456:ecs-deploy-topic'
|
|
191
|
+
|
|
192
|
+
# ECS service identifier - suffix matched against resource ARNs in events.
|
|
193
|
+
# config.aws_service_identifier = 'myapp-production-web'
|
|
194
|
+
|
|
195
|
+
# Commit hash of the currently deploying build. Prefix-matched against
|
|
196
|
+
# issue metadata commit_sha on SERVICE_DEPLOYMENT_COMPLETED events.
|
|
197
|
+
# config.production_commit_sha = Rails.configuration.x.image_tag
|
|
198
|
+
|
|
199
|
+
# Toggle for processing AWS webhook events (default: Rails.env.production?).
|
|
200
|
+
# config.process_aws_webhooks = Rails.env.production?
|
|
201
|
+
|
|
202
|
+
# SNS signature verifier (default: Aws::SNS::MessageVerifier). Must respond
|
|
203
|
+
# to authenticate!(raw_body). Provide aws-sdk-sns in your Gemfile.
|
|
204
|
+
# config.sns_verifier_class = Aws::SNS::MessageVerifier
|
|
205
|
+
# config.sns_verifier_error = Aws::SNS::MessageVerifier::VerificationError
|
|
206
|
+
|
|
207
|
+
# --------------------------------------------------------------------------
|
|
208
|
+
# Route mounting
|
|
209
|
+
# --------------------------------------------------------------------------
|
|
210
|
+
# Per-group toggles for which engine route groups to mount.
|
|
211
|
+
# config.mount_groups = { webhooks: true, issues: true, projects: true }
|
|
212
|
+
|
|
213
|
+
# --------------------------------------------------------------------------
|
|
214
|
+
# Controller overrides (Devise-style)
|
|
215
|
+
# --------------------------------------------------------------------------
|
|
216
|
+
# Swap in your own controller for any mounted route group. Typical use is
|
|
217
|
+
# to subclass the gem controller and slot the subclass in here:
|
|
218
|
+
#
|
|
219
|
+
# # app/controllers/issues_controller.rb
|
|
220
|
+
# class IssuesController < PlanMyStuff::IssuesController
|
|
221
|
+
# before_action :require_admin
|
|
222
|
+
# end
|
|
223
|
+
#
|
|
224
|
+
# # config/initializers/plan_my_stuff.rb
|
|
225
|
+
# config.controllers = { issues: 'issues' }
|
|
226
|
+
#
|
|
227
|
+
# Unset keys fall back to the gem default.
|
|
228
|
+
#
|
|
229
|
+
# Controllable keys (with gem defaults):
|
|
230
|
+
# :issues => 'plan_my_stuff/issues'
|
|
231
|
+
# :comments => 'plan_my_stuff/comments'
|
|
232
|
+
# :labels => 'plan_my_stuff/labels'
|
|
233
|
+
# :projects => 'plan_my_stuff/projects'
|
|
234
|
+
# :project_items => 'plan_my_stuff/project_items'
|
|
235
|
+
# :testing_projects => 'plan_my_stuff/testing_projects'
|
|
236
|
+
# :testing_project_items => 'plan_my_stuff/testing_project_items'
|
|
237
|
+
# :'issues/closures' => 'plan_my_stuff/issues/closures'
|
|
238
|
+
# :'issues/viewers' => 'plan_my_stuff/issues/viewers'
|
|
239
|
+
# :'issues/takes' => 'plan_my_stuff/issues/takes'
|
|
240
|
+
# :'issues/waitings' => 'plan_my_stuff/issues/waitings'
|
|
241
|
+
# :'issues/links' => 'plan_my_stuff/issues/links'
|
|
242
|
+
# :'issues/approvals' => 'plan_my_stuff/issues/approvals'
|
|
243
|
+
# :'project_items/statuses' => 'plan_my_stuff/project_items/statuses'
|
|
244
|
+
# :'project_items/assignments' => 'plan_my_stuff/project_items/assignments'
|
|
245
|
+
# :'testing_project_items/results' => 'plan_my_stuff/testing_project_items/results'
|
|
246
|
+
# :'webhooks/github' => 'plan_my_stuff/webhooks/github'
|
|
247
|
+
# :'webhooks/aws' => 'plan_my_stuff/webhooks/aws'
|
|
115
248
|
end
|
|
@@ -7,6 +7,13 @@ module PlanMyStuff
|
|
|
7
7
|
# Provides shared persistence predicates and utility helpers.
|
|
8
8
|
class ApplicationRecord
|
|
9
9
|
include ActiveModel::Model
|
|
10
|
+
include ActiveModel::Attributes
|
|
11
|
+
include ActiveModel::Dirty
|
|
12
|
+
include ActiveModel::Serializers::JSON
|
|
13
|
+
|
|
14
|
+
# @return [Object, nil] raw GitHub API response this record was hydrated from.
|
|
15
|
+
# Escape hatch for consuming apps to access fields the gem doesn't expose.
|
|
16
|
+
attr_reader :github_response
|
|
10
17
|
|
|
11
18
|
class << self
|
|
12
19
|
# Reads a field from an object that may respond to method calls or hash access.
|
|
@@ -19,11 +26,96 @@ module PlanMyStuff
|
|
|
19
26
|
def read_field(obj, field)
|
|
20
27
|
obj.respond_to?(field) ? obj.public_send(field) : obj[field]
|
|
21
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# @param client [PlanMyStuff::Client]
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
#
|
|
36
|
+
def not_modified?(client)
|
|
37
|
+
response = client.last_response
|
|
38
|
+
return false if response.nil?
|
|
39
|
+
|
|
40
|
+
response.status == 304
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Captures the +ETag+ header from the most recent REST response and
|
|
44
|
+
# forwards it to +cache_writer+.
|
|
45
|
+
#
|
|
46
|
+
# @param client [PlanMyStuff::Client]
|
|
47
|
+
# @param repo [String]
|
|
48
|
+
# @param id [Integer, nil]
|
|
49
|
+
# @param body [Object] parsed GitHub response
|
|
50
|
+
# @param cache_writer [Symbol] +PlanMyStuff::Cache+ method name, e.g. +:write_issue+
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
53
|
+
#
|
|
54
|
+
def store_etag_to_cache(client, repo, id, body, cache_writer:)
|
|
55
|
+
return if id.nil?
|
|
56
|
+
|
|
57
|
+
response = client.last_response
|
|
58
|
+
return if response.nil?
|
|
59
|
+
|
|
60
|
+
etag = response.headers && response.headers['etag']
|
|
61
|
+
return if etag.blank?
|
|
62
|
+
|
|
63
|
+
PlanMyStuff::Cache.public_send(cache_writer, repo, id, etag: etag, body: body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Captures the +ETag+ header from the most recent REST response and
|
|
67
|
+
# forwards it to +Cache.write_list+.
|
|
68
|
+
#
|
|
69
|
+
# @param client [PlanMyStuff::Client]
|
|
70
|
+
# @param resource [Symbol] :issue or :comment
|
|
71
|
+
# @param repo [String]
|
|
72
|
+
# @param params [Hash] query params that identify this list
|
|
73
|
+
# @param body [Object] parsed GitHub list response
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
77
|
+
def store_list_etag_to_cache(client, resource, repo, params, body)
|
|
78
|
+
response = client.last_response
|
|
79
|
+
return if response.nil?
|
|
80
|
+
|
|
81
|
+
etag = response.headers && response.headers['etag']
|
|
82
|
+
return if etag.blank?
|
|
83
|
+
|
|
84
|
+
PlanMyStuff::Cache.write_list(resource, repo, params, etag: etag, body: body)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Reads a REST resource through the ETag cache.
|
|
88
|
+
#
|
|
89
|
+
# On a cache hit sends +If-None-Match+; a 304 returns the cached
|
|
90
|
+
# body. A 200 stores the new ETag via +cache_writer+ and returns
|
|
91
|
+
# the fresh result.
|
|
92
|
+
#
|
|
93
|
+
# @param client [PlanMyStuff::Client]
|
|
94
|
+
# @param repo [String]
|
|
95
|
+
# @param id [Integer]
|
|
96
|
+
# @param rest_method [Symbol] Octokit method name
|
|
97
|
+
# @param cache_reader [Symbol] +PlanMyStuff::Cache+ method name, e.g. +:read_issue+
|
|
98
|
+
# @param cache_writer [Symbol] +PlanMyStuff::Cache+ method name, e.g. +:write_issue+
|
|
99
|
+
#
|
|
100
|
+
# @return [Object] parsed GitHub response
|
|
101
|
+
#
|
|
102
|
+
def fetch_with_etag_cache(client, repo, id, rest_method:, cache_reader:, cache_writer:)
|
|
103
|
+
cached = PlanMyStuff::Cache.public_send(cache_reader, repo, id)
|
|
104
|
+
options = cached ? { headers: { 'If-None-Match' => cached[:etag] } } : {}
|
|
105
|
+
|
|
106
|
+
result = client.rest(rest_method, repo, id, **options)
|
|
107
|
+
|
|
108
|
+
return cached[:body] if cached && not_modified?(client)
|
|
109
|
+
|
|
110
|
+
store_etag_to_cache(client, repo, id, result, cache_writer: cache_writer)
|
|
111
|
+
result
|
|
112
|
+
end
|
|
22
113
|
end
|
|
23
114
|
|
|
24
115
|
def initialize(**)
|
|
25
116
|
super
|
|
26
117
|
@persisted = false
|
|
118
|
+
@destroyed = false
|
|
27
119
|
end
|
|
28
120
|
|
|
29
121
|
# @return [Boolean]
|
|
@@ -36,8 +128,37 @@ module PlanMyStuff
|
|
|
36
128
|
!@persisted
|
|
37
129
|
end
|
|
38
130
|
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def destroyed?
|
|
133
|
+
@destroyed
|
|
134
|
+
end
|
|
135
|
+
|
|
39
136
|
private
|
|
40
137
|
|
|
138
|
+
# Marks this record as persisted. Subclasses call this after a
|
|
139
|
+
# successful create/find/update against the underlying GitHub
|
|
140
|
+
# resource. Also applies any pending dirty changes so the record
|
|
141
|
+
# is clean after hydration and +#previous_changes+ reflects the
|
|
142
|
+
# values that were just loaded.
|
|
143
|
+
#
|
|
144
|
+
# @return [void]
|
|
145
|
+
#
|
|
146
|
+
def persisted!
|
|
147
|
+
@persisted = true
|
|
148
|
+
changes_applied
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Marks this record as destroyed. Subclasses call this from their
|
|
152
|
+
# +destroy!+ implementations after the underlying remote resource
|
|
153
|
+
# has been deleted.
|
|
154
|
+
#
|
|
155
|
+
# @return [void]
|
|
156
|
+
#
|
|
157
|
+
def destroyed!
|
|
158
|
+
@destroyed = true
|
|
159
|
+
@persisted = false
|
|
160
|
+
end
|
|
161
|
+
|
|
41
162
|
# Reads a field from an object that may respond to method calls or hash access.
|
|
42
163
|
#
|
|
43
164
|
# @param obj [Object]
|