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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module PlanMyStuff
|
|
6
|
+
module Webhooks
|
|
7
|
+
class GithubController < ActionController::API
|
|
8
|
+
before_action :verify_signature!
|
|
9
|
+
|
|
10
|
+
# POST /webhooks/github
|
|
11
|
+
def create
|
|
12
|
+
event = request.headers['X-GitHub-Event']
|
|
13
|
+
|
|
14
|
+
case event
|
|
15
|
+
when 'pull_request'
|
|
16
|
+
handle_pull_request
|
|
17
|
+
when 'issues'
|
|
18
|
+
handle_issues
|
|
19
|
+
when 'projects_v2_item'
|
|
20
|
+
handle_projects_v2_item
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
head(:ok)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Validates HMAC-SHA256 signature from the X-Hub-Signature-256 header.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
def verify_signature!
|
|
33
|
+
body = request.body.read
|
|
34
|
+
request.body.rewind
|
|
35
|
+
|
|
36
|
+
signature = request.headers['X-Hub-Signature-256'].to_s.gsub('sha256=', '')
|
|
37
|
+
if signature.blank?
|
|
38
|
+
head(:unauthorized)
|
|
39
|
+
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
digest = OpenSSL::Digest.new('sha256')
|
|
44
|
+
secret = PlanMyStuff.configuration.webhook_secret
|
|
45
|
+
hmac = OpenSSL::HMAC.hexdigest(digest, secret, body)
|
|
46
|
+
|
|
47
|
+
return if ActiveSupport::SecurityUtils.secure_compare(hmac, signature)
|
|
48
|
+
|
|
49
|
+
head(:unauthorized)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [ActionController::Parameters]
|
|
53
|
+
def payload_params
|
|
54
|
+
@payload_params ||=
|
|
55
|
+
begin
|
|
56
|
+
body = request.body.read
|
|
57
|
+
request.body.rewind
|
|
58
|
+
ActionController::Parameters.new(JSON.parse(body))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [ActionController::Parameters]
|
|
63
|
+
def pull_request_params
|
|
64
|
+
payload_params.fetch(:pull_request)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [ActionController::Parameters]
|
|
68
|
+
def issue_params
|
|
69
|
+
payload_params.fetch(:issue)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [void]
|
|
73
|
+
def handle_issues
|
|
74
|
+
action = payload_params.fetch(:action)
|
|
75
|
+
|
|
76
|
+
case action
|
|
77
|
+
when 'assigned'
|
|
78
|
+
handle_issue_assigned
|
|
79
|
+
when 'unassigned'
|
|
80
|
+
handle_issue_unassigned
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Adds the issue to the pipeline project at "Submitted" the first
|
|
85
|
+
# time it is assigned. Re-assigns and additional assignees are
|
|
86
|
+
# ignored -- if the issue is already in the pipeline we do not
|
|
87
|
+
# touch its status.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
#
|
|
91
|
+
def handle_issue_assigned
|
|
92
|
+
issue_number = issue_params.fetch(:number)
|
|
93
|
+
assignee_login = payload_params.dig(:assignee, :login)
|
|
94
|
+
|
|
95
|
+
return if assignee_login.blank?
|
|
96
|
+
|
|
97
|
+
existing = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
98
|
+
if existing.present?
|
|
99
|
+
Rails.logger.info("[PlanMyStuff] Issue ##{issue_number} already in pipeline project, skipping submit!")
|
|
100
|
+
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
repo = payload_params.dig(:repository, :full_name)
|
|
105
|
+
issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
|
|
106
|
+
PlanMyStuff::Pipeline.submit!(issue, assignee: assignee_login)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Removes the issue from the pipeline project when the LAST
|
|
110
|
+
# assignee is removed. If any assignees remain, the webhook is
|
|
111
|
+
# a no-op (only one of N was removed). If the issue isn't in
|
|
112
|
+
# the pipeline at all, also a no-op (logged at info).
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
#
|
|
116
|
+
def handle_issue_unassigned
|
|
117
|
+
remaining_assignees = Array.wrap(issue_params[:assignees])
|
|
118
|
+
return if remaining_assignees.any?
|
|
119
|
+
|
|
120
|
+
issue_number = issue_params.fetch(:number)
|
|
121
|
+
project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
122
|
+
|
|
123
|
+
if project_item.nil?
|
|
124
|
+
Rails.logger.info(
|
|
125
|
+
"[PlanMyStuff] Issue ##{issue_number} not in pipeline project, skipping remove!",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
PlanMyStuff::Pipeline.remove!(project_item)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Handles GitHub's +projects_v2_item+ webhook event. Mirrors a dev
|
|
135
|
+
# dragging an item from "Submitted" to "Started" on the project
|
|
136
|
+
# board into a +Pipeline.take!+ call so the
|
|
137
|
+
# +plan_my_stuff.pipeline.started+ event fires.
|
|
138
|
+
#
|
|
139
|
+
# Only acts on +action: 'edited'+ where the Status single-select
|
|
140
|
+
# field changed on the pipeline project. No-ops when the new
|
|
141
|
+
# status is anything other than "Started", when the +from+ status
|
|
142
|
+
# is already "Started" (loop guard from +move_to!+ triggering
|
|
143
|
+
# another webhook), or when the item cannot be located on the
|
|
144
|
+
# pipeline project.
|
|
145
|
+
#
|
|
146
|
+
# @return [void]
|
|
147
|
+
#
|
|
148
|
+
def handle_projects_v2_item
|
|
149
|
+
return unless payload_params[:action] == 'edited'
|
|
150
|
+
|
|
151
|
+
item_project_node_id = payload_params.dig(:projects_v2_item, :project_node_id)
|
|
152
|
+
return if item_project_node_id.blank?
|
|
153
|
+
|
|
154
|
+
pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
|
|
155
|
+
project = PlanMyStuff::Project.find(pipeline_number)
|
|
156
|
+
return unless project.id == item_project_node_id
|
|
157
|
+
|
|
158
|
+
field_value = payload_params.dig(:changes, :field_value)
|
|
159
|
+
return if field_value.blank?
|
|
160
|
+
|
|
161
|
+
return unless field_value[:field_name] == 'Status'
|
|
162
|
+
|
|
163
|
+
started_name = PlanMyStuff::Pipeline.resolve_status_name(
|
|
164
|
+
PlanMyStuff::Pipeline::Status::STARTED,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
to_name = field_value.dig(:to, :name)
|
|
168
|
+
from_name = field_value.dig(:from, :name)
|
|
169
|
+
|
|
170
|
+
# Moved from 'Started'
|
|
171
|
+
return if from_name == started_name
|
|
172
|
+
|
|
173
|
+
# Moved to something other than 'Started'
|
|
174
|
+
return unless to_name == started_name
|
|
175
|
+
|
|
176
|
+
item_node_id = payload_params.dig(:projects_v2_item, :node_id)
|
|
177
|
+
return if item_node_id.blank?
|
|
178
|
+
|
|
179
|
+
project_item = project.items.find { |item| item.id == item_node_id }
|
|
180
|
+
|
|
181
|
+
if project_item.nil?
|
|
182
|
+
Rails.logger.info(
|
|
183
|
+
"[PlanMyStuff] projects_v2_item #{item_node_id} not found in pipeline project, skipping take!",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
PlanMyStuff::Pipeline.take!(project_item)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [void]
|
|
193
|
+
def handle_pull_request
|
|
194
|
+
action = payload_params.fetch(:action)
|
|
195
|
+
|
|
196
|
+
# Only PRs targeting +main+ drive pipeline transitions on
|
|
197
|
+
# open/draft/reopen. PRs into +production+ are deploy PRs
|
|
198
|
+
# (often auto-created as drafts) and must not bump items
|
|
199
|
+
# back to "Started". The +closed+ branch handles its own
|
|
200
|
+
# base-ref routing.
|
|
201
|
+
if %w[opened ready_for_review reopened converted_to_draft].include?(action)
|
|
202
|
+
base_ref = pull_request_params.dig(:base, :ref)
|
|
203
|
+
return unless base_ref == PlanMyStuff.configuration.main_branch
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
case action
|
|
207
|
+
when 'opened'
|
|
208
|
+
pull_request_params[:draft] ? handle_converted_to_draft : handle_ready_for_review
|
|
209
|
+
when 'ready_for_review', 'reopened'
|
|
210
|
+
handle_ready_for_review
|
|
211
|
+
when 'converted_to_draft'
|
|
212
|
+
handle_converted_to_draft
|
|
213
|
+
when 'closed'
|
|
214
|
+
handle_closed
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [void]
|
|
219
|
+
def handle_ready_for_review
|
|
220
|
+
each_linked_item do |item|
|
|
221
|
+
PlanMyStuff::Pipeline.mark_in_review!(item)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @return [void]
|
|
226
|
+
def handle_converted_to_draft
|
|
227
|
+
each_linked_item do |item|
|
|
228
|
+
PlanMyStuff::Pipeline.take!(item)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# @return [void]
|
|
233
|
+
def handle_closed
|
|
234
|
+
return unless pull_request_params[:merged]
|
|
235
|
+
|
|
236
|
+
base_ref = pull_request_params.dig(:base, :ref)
|
|
237
|
+
config = PlanMyStuff.configuration
|
|
238
|
+
|
|
239
|
+
if base_ref == config.main_branch
|
|
240
|
+
each_linked_item do |item|
|
|
241
|
+
PlanMyStuff::Pipeline.mark_ready_for_release!(item)
|
|
242
|
+
end
|
|
243
|
+
elsif base_ref == config.production_branch
|
|
244
|
+
PlanMyStuff::Pipeline.start_deployment!(commit_sha: pull_request_params[:merge_commit_sha])
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @yield [PlanMyStuff::ProjectItem]
|
|
249
|
+
def each_linked_item
|
|
250
|
+
messages = fetch_commit_messages
|
|
251
|
+
numbers = PlanMyStuff::Pipeline::IssueLinker.extract_issue_numbers(
|
|
252
|
+
pull_request_params,
|
|
253
|
+
commit_messages: messages,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
numbers.each do |issue_number|
|
|
257
|
+
item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
258
|
+
|
|
259
|
+
if item
|
|
260
|
+
yield(item)
|
|
261
|
+
else
|
|
262
|
+
Rails.logger.info("[PlanMyStuff] Issue ##{issue_number} not found in pipeline project, skipping")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Fetches commit messages for the current PR via the GitHub API.
|
|
268
|
+
#
|
|
269
|
+
# @return [Array<String>] commit messages (empty on failure)
|
|
270
|
+
#
|
|
271
|
+
def fetch_commit_messages
|
|
272
|
+
repo = payload_params.dig(:repository, :full_name)
|
|
273
|
+
pr_number = pull_request_params[:number]
|
|
274
|
+
|
|
275
|
+
# TODO? Add a PR class
|
|
276
|
+
commits = PlanMyStuff.client.rest(:pull_request_commits, repo, pr_number)
|
|
277
|
+
commits.map { |c| c[:commit][:message] }
|
|
278
|
+
rescue PlanMyStuff::APIError => e
|
|
279
|
+
Rails.logger.warn("[PlanMyStuff] Failed to fetch PR commits: #{e.message}")
|
|
280
|
+
[]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Base class for all PMS gem jobs. Subclasses +::ActiveJob::Base+ so
|
|
5
|
+
# the gem doesn't depend on the consuming app having an
|
|
6
|
+
# +ApplicationJob+ constant.
|
|
7
|
+
class ApplicationJob < ::ActiveJob::Base
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Daily-cadence sweep job for reminder dispatch + inactivity auto-close.
|
|
5
|
+
# Consuming apps enqueue it once; the job self-requeues after each
|
|
6
|
+
# perform so the schedule persists without requiring a cron/whenever
|
|
7
|
+
# setup. Queue-adapter-agnostic: runs through ActiveJob's
|
|
8
|
+
# +set(wait_until:).perform_later+ so any backend works.
|
|
9
|
+
class RemindersSweepJob < PlanMyStuff::ApplicationJob
|
|
10
|
+
queue_as :default
|
|
11
|
+
|
|
12
|
+
# Only try once per enqueue. Without this, Delayed-style adapters
|
|
13
|
+
# retry up to 25 times on error and our +around_perform+ +ensure+
|
|
14
|
+
# re-enqueues a follow-up run on every attempt, causing geometric
|
|
15
|
+
# duplicate pile-up. With +attempts: 1+, exactly one perform per
|
|
16
|
+
# +perform_later+; if it fails, the follow-up scheduled in +ensure+
|
|
17
|
+
# takes over tomorrow.
|
|
18
|
+
retry_on StandardError, attempts: 1
|
|
19
|
+
|
|
20
|
+
around_perform :requeue_for_next_run
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Next sweep time. Default: 6:30am Eastern tomorrow (today if the
|
|
24
|
+
# current time is before 6:30am ET). Apps wanting a different
|
|
25
|
+
# cadence override this on a subclass.
|
|
26
|
+
#
|
|
27
|
+
# @return [Time] UTC
|
|
28
|
+
#
|
|
29
|
+
def next_run
|
|
30
|
+
now = Time.current.in_time_zone('Eastern Time (US & Canada)')
|
|
31
|
+
target = now.change(hour: 6, min: 30, sec: 0)
|
|
32
|
+
target += 1.day if target <= now
|
|
33
|
+
target.utc
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Schedules a sweep for +next_run+. Used by the after-perform
|
|
37
|
+
# self-requeue and the +plan_my_stuff:reminders:sweep+ rake task so
|
|
38
|
+
# both kick off runs on the same cadence (instead of "now").
|
|
39
|
+
#
|
|
40
|
+
# @param repo [Symbol, String]
|
|
41
|
+
#
|
|
42
|
+
# @return [ActiveJob::Base]
|
|
43
|
+
#
|
|
44
|
+
def requeue(repo)
|
|
45
|
+
set(wait_until: next_run).perform_later(repo)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param repo [Symbol, String]
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
#
|
|
53
|
+
def perform(repo)
|
|
54
|
+
@repo_arg = repo
|
|
55
|
+
PlanMyStuff::Reminders::Sweep.new(repo: repo).call
|
|
56
|
+
PlanMyStuff::Archive::Sweep.new(repo: repo).call
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Runs inside +around_perform+. Re-enqueues the next run in an
|
|
62
|
+
# +ensure+ so a perform error still schedules the next one. Skips
|
|
63
|
+
# requeue when perform never captured a repo arg (deserialization
|
|
64
|
+
# error or direct +.perform+ call with missing kwargs).
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
#
|
|
68
|
+
def requeue_for_next_run
|
|
69
|
+
yield
|
|
70
|
+
ensure
|
|
71
|
+
enqueue_next_run
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [void]
|
|
75
|
+
def enqueue_next_run
|
|
76
|
+
return if @repo_arg.nil?
|
|
77
|
+
|
|
78
|
+
self.class.requeue(@repo_arg)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<%
|
|
2
|
+
approvers = issue.approvers
|
|
3
|
+
pending_count = issue.pending_approvals.size
|
|
4
|
+
current_user_id = current_user_id_local
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<% if issue.approvals_required? || support_user %>
|
|
8
|
+
<section aria-label="Manager approvals">
|
|
9
|
+
<h3>Approvals</h3>
|
|
10
|
+
|
|
11
|
+
<% if issue.approvals_required? %>
|
|
12
|
+
<% if issue.fully_approved? %>
|
|
13
|
+
<p><strong>Fully approved</strong></p>
|
|
14
|
+
<% else %>
|
|
15
|
+
<p><strong><%= pending_count %> of <%= approvers.size %> approval(s) pending</strong></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<ul>
|
|
19
|
+
<% approvers.each do |approval| %>
|
|
20
|
+
<li>
|
|
21
|
+
<%= PlanMyStuff::UserResolver.display_name(approval.user) %> —
|
|
22
|
+
<% if approval.approved? %>
|
|
23
|
+
approved
|
|
24
|
+
<% if approval.approved_at %>
|
|
25
|
+
at <%= approval.approved_at.iso8601 %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% else %>
|
|
28
|
+
pending
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% if approval.pending? && approval.user_id == current_user_id %>
|
|
32
|
+
<%=
|
|
33
|
+
button_to(
|
|
34
|
+
'Approve',
|
|
35
|
+
plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
|
|
36
|
+
method: :patch,
|
|
37
|
+
params: { approval: { status: 'approved' } },
|
|
38
|
+
form: { style: 'display:inline' },
|
|
39
|
+
)
|
|
40
|
+
%>
|
|
41
|
+
<% end %>
|
|
42
|
+
|
|
43
|
+
<% if approval.approved? && (approval.user_id == current_user_id || support_user) %>
|
|
44
|
+
<%=
|
|
45
|
+
button_to(
|
|
46
|
+
'Revoke',
|
|
47
|
+
plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
|
|
48
|
+
method: :patch,
|
|
49
|
+
params: { approval: { status: 'pending' } },
|
|
50
|
+
form: { style: 'display:inline' },
|
|
51
|
+
)
|
|
52
|
+
%>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<% if support_user %>
|
|
56
|
+
<%=
|
|
57
|
+
button_to(
|
|
58
|
+
'Remove',
|
|
59
|
+
plan_my_stuff.issue_approval_path(issue.number, approval.user_id, repo: issue.repo.full_name),
|
|
60
|
+
method: :delete,
|
|
61
|
+
form: { style: 'display:inline' },
|
|
62
|
+
)
|
|
63
|
+
%>
|
|
64
|
+
<% end %>
|
|
65
|
+
</li>
|
|
66
|
+
<% end %>
|
|
67
|
+
</ul>
|
|
68
|
+
<% else %>
|
|
69
|
+
<p><em>No approvers required</em></p>
|
|
70
|
+
<% end %>
|
|
71
|
+
|
|
72
|
+
<% if support_user %>
|
|
73
|
+
<%=
|
|
74
|
+
form_with(
|
|
75
|
+
scope: :approval,
|
|
76
|
+
url: plan_my_stuff.issue_approvals_path(issue.number, repo: issue.repo.full_name),
|
|
77
|
+
method: :post,
|
|
78
|
+
local: true,
|
|
79
|
+
) do |form|
|
|
80
|
+
%>
|
|
81
|
+
<%= form.label(:user_ids, 'Add approver IDs (comma-separated)') %>
|
|
82
|
+
<%= form.text_field(:user_ids) %>
|
|
83
|
+
<%= form.submit('Add Approvers') %>
|
|
84
|
+
<% end %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</section>
|
|
87
|
+
<% end %>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<%=
|
|
6
6
|
button_to(
|
|
7
7
|
'x',
|
|
8
|
-
plan_my_stuff.
|
|
8
|
+
plan_my_stuff.issue_label_path(issue.number, label),
|
|
9
9
|
method: :delete,
|
|
10
10
|
form: { style: 'display:inline' }
|
|
11
11
|
)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<em>No labels</em>
|
|
17
17
|
<% end %>
|
|
18
18
|
|
|
19
|
-
<%= form_with(url: plan_my_stuff.
|
|
19
|
+
<%= form_with(url: plan_my_stuff.issue_labels_path(issue.number), method: :post, local: true) do |form| %>
|
|
20
20
|
<%= form.label(:label_name, 'Add label') %>
|
|
21
21
|
<%= form.text_field(:label_name) %>
|
|
22
22
|
<%= form.submit('Add') %>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# read_only sections have no add form; remove_only sections allow X but
|
|
3
|
+
# no add (blocking is created from the blocked side; duplicate_of is
|
|
4
|
+
# created via mark_duplicate! and cleared by reopening on GitHub).
|
|
5
|
+
sections = [
|
|
6
|
+
{ type: 'parent', label: 'Parent', single: true, add_form: true, removable: true, targets: [issue.parent].compact },
|
|
7
|
+
{ type: 'sub_ticket', label: 'Sub-tickets', single: false, add_form: true, removable: true, targets: issue.sub_tickets },
|
|
8
|
+
{ type: 'blocked_by', label: 'Blocked by', single: false, add_form: true, removable: true, targets: issue.blocked_by },
|
|
9
|
+
{ type: 'blocking', label: 'Blocking', single: false, add_form: false, removable: false, targets: issue.blocking },
|
|
10
|
+
{ type: 'related', label: 'Related', single: false, add_form: true, removable: true, targets: issue.related },
|
|
11
|
+
{ type: 'duplicate_of', label: 'Duplicate of', single: true, add_form: false, removable: false, targets: [issue.duplicate_of].compact },
|
|
12
|
+
]
|
|
13
|
+
visible_sections = support_user ? sections : sections.select { |s| s[:type] == 'related' }
|
|
14
|
+
%>
|
|
15
|
+
|
|
16
|
+
<section aria-label="Ticket links">
|
|
17
|
+
<h3>Links</h3>
|
|
18
|
+
<% visible_sections.each do |section| %>
|
|
19
|
+
<div>
|
|
20
|
+
<h4><%= section[:label] %></h4>
|
|
21
|
+
<% if section[:targets].any? %>
|
|
22
|
+
<ul>
|
|
23
|
+
<% section[:targets].each do |target| %>
|
|
24
|
+
<li>
|
|
25
|
+
<%=
|
|
26
|
+
link_to(
|
|
27
|
+
"#{target.repo.full_name}##{target.number} - #{target.title}",
|
|
28
|
+
plan_my_stuff.issue_path(target.number, repo: target.repo.full_name),
|
|
29
|
+
)
|
|
30
|
+
%>
|
|
31
|
+
<% if section[:removable] %>
|
|
32
|
+
<%
|
|
33
|
+
link_id = "#{section[:type]}:#{target.repo.full_name}:#{target.number}"
|
|
34
|
+
%>
|
|
35
|
+
<%=
|
|
36
|
+
button_to(
|
|
37
|
+
'Remove',
|
|
38
|
+
plan_my_stuff.issue_link_path(issue.number, link_id, repo: issue.repo.full_name),
|
|
39
|
+
method: :delete,
|
|
40
|
+
form: { style: 'display:inline' },
|
|
41
|
+
)
|
|
42
|
+
%>
|
|
43
|
+
<% end %>
|
|
44
|
+
</li>
|
|
45
|
+
<% end %>
|
|
46
|
+
</ul>
|
|
47
|
+
<% else %>
|
|
48
|
+
<p><em>None</em></p>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<% if section[:add_form] && (!section[:single] || section[:targets].empty?) %>
|
|
52
|
+
<%=
|
|
53
|
+
form_with(
|
|
54
|
+
scope: :link,
|
|
55
|
+
url: plan_my_stuff.issue_links_path(issue.number, repo: issue.repo.full_name),
|
|
56
|
+
method: :post,
|
|
57
|
+
local: true,
|
|
58
|
+
) do |form|
|
|
59
|
+
%>
|
|
60
|
+
<%= form.hidden_field(:type, value: section[:type]) %>
|
|
61
|
+
<%= form.label(:issue_number, 'Issue #') %>
|
|
62
|
+
<%= form.number_field(:issue_number) %>
|
|
63
|
+
<%= form.label(:repo, 'Repo (optional)') %>
|
|
64
|
+
<%= form.text_field(:repo, placeholder: issue.repo.full_name) %>
|
|
65
|
+
<%= form.submit("Add #{section[:label].downcase}") %>
|
|
66
|
+
<% end %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</section>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<%=
|
|
10
10
|
button_to(
|
|
11
11
|
'Remove',
|
|
12
|
-
plan_my_stuff.
|
|
12
|
+
plan_my_stuff.issue_viewer_path(issue.number, viewer_id, repo: issue.repo.full_name),
|
|
13
13
|
method: :delete
|
|
14
14
|
)
|
|
15
15
|
%>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<p>No viewers added.</p>
|
|
21
21
|
<% end %>
|
|
22
22
|
|
|
23
|
-
<%= form_with(url: plan_my_stuff.
|
|
23
|
+
<%= form_with(url: plan_my_stuff.issue_viewers_path(issue.number, repo: issue.repo.full_name), method: :post) do |form| %>
|
|
24
24
|
<div>
|
|
25
25
|
<%= form.label(:viewer_ids, 'Add viewer IDs (comma-separated)') %>
|
|
26
26
|
<%= form.text_field(:viewer_ids) %>
|
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
<p>
|
|
4
4
|
<%= link_to('Edit', plan_my_stuff.edit_issue_path(@issue.number, repo: @issue.repo.full_name)) %>
|
|
5
|
+
<% if @support_user && @issue.html_url.present? %>
|
|
6
|
+
<%= link_to('View on GitHub', @issue.html_url, target: '_blank', rel: 'noopener') %>
|
|
7
|
+
<% end %>
|
|
5
8
|
<% if @issue.state == 'open' %>
|
|
6
|
-
<%= button_to('Close Issue', plan_my_stuff.
|
|
9
|
+
<%= button_to('Close Issue', plan_my_stuff.issue_closure_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
|
|
7
10
|
<% else %>
|
|
8
|
-
<%= button_to('Reopen Issue', plan_my_stuff.
|
|
11
|
+
<%= button_to('Reopen Issue', plan_my_stuff.issue_closure_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% if @support_user && @issue.state == 'open' %>
|
|
14
|
+
<% if @issue.metadata.waiting_on_user_at.present? %>
|
|
15
|
+
<%= button_to('Mark replied', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :delete) %>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= button_to('Mark waiting', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% if @support_user && @pipeline_enabled && (@pipeline_item.nil? || @pipeline_item.status == PlanMyStuff::Pipeline.resolve_status_name(PlanMyStuff::Pipeline::Status::SUBMITTED)) %>
|
|
21
|
+
<%= button_to('Take', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% if @support_user %>
|
|
24
|
+
<%= link_to('Start Testing Project', plan_my_stuff.new_testing_project_path(subject_url: @issue.html_url)) %>
|
|
9
25
|
<% end %>
|
|
10
26
|
</p>
|
|
11
27
|
|
|
@@ -27,6 +43,28 @@
|
|
|
27
43
|
|
|
28
44
|
<hr>
|
|
29
45
|
|
|
46
|
+
<%=
|
|
47
|
+
render({
|
|
48
|
+
partial: 'plan_my_stuff/issues/partials/links',
|
|
49
|
+
locals: { issue: @issue, support_user: @support_user },
|
|
50
|
+
})
|
|
51
|
+
%>
|
|
52
|
+
|
|
53
|
+
<hr>
|
|
54
|
+
|
|
55
|
+
<%=
|
|
56
|
+
render({
|
|
57
|
+
partial: 'plan_my_stuff/issues/partials/approvals',
|
|
58
|
+
locals: {
|
|
59
|
+
issue: @issue,
|
|
60
|
+
support_user: @support_user,
|
|
61
|
+
current_user_id_local: @current_user_id,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
%>
|
|
65
|
+
|
|
66
|
+
<hr>
|
|
67
|
+
|
|
30
68
|
<h2>Comments (<%= @comments.size %>)</h2>
|
|
31
69
|
|
|
32
70
|
<% if @comments.any? %>
|
|
@@ -38,6 +76,11 @@
|
|
|
38
76
|
<%= link_to('Edit', plan_my_stuff.edit_issue_comment_path(@issue.number, comment.id, repo: @issue.repo.full_name)) %>
|
|
39
77
|
</p>
|
|
40
78
|
<% end %>
|
|
79
|
+
<% if @support_user && comment.html_url.present? %>
|
|
80
|
+
<p>
|
|
81
|
+
<%= link_to('View on GitHub', comment.html_url, target: '_blank', rel: 'noopener') %>
|
|
82
|
+
</p>
|
|
83
|
+
<% end %>
|
|
41
84
|
</div>
|
|
42
85
|
<% end %>
|
|
43
86
|
<% else %>
|
|
@@ -51,7 +94,7 @@
|
|
|
51
94
|
partial: 'plan_my_stuff/comments/partials/form',
|
|
52
95
|
locals: {
|
|
53
96
|
issue: @issue,
|
|
54
|
-
comment: PlanMyStuff::Comment.new,
|
|
97
|
+
comment: PlanMyStuff::Comment.new(visibility: :public),
|
|
55
98
|
support_user: @support_user,
|
|
56
99
|
},
|
|
57
100
|
})
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
<h1>Projects</h1>
|
|
2
2
|
|
|
3
|
+
<p>
|
|
4
|
+
<%= link_to('All', plan_my_stuff.projects_path(filter: 'all')) %>
|
|
5
|
+
<%= link_to('Regular', plan_my_stuff.projects_path(filter: 'regular')) %>
|
|
6
|
+
<%= link_to('Testing', plan_my_stuff.projects_path(filter: 'testing')) %>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
3
9
|
<% if @projects.any? %>
|
|
4
10
|
<ul>
|
|
5
11
|
<% @projects.each do |project| %>
|
|
6
12
|
<li>
|
|
7
|
-
|
|
13
|
+
<% if project.is_a?(PlanMyStuff::TestingProject) %>
|
|
14
|
+
<%= link_to(project.title, plan_my_stuff.testing_project_path(project.number)) %>
|
|
15
|
+
<span>[Testing]</span>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= link_to(project.title, plan_my_stuff.project_path(project.number)) %>
|
|
18
|
+
<% end %>
|
|
8
19
|
</li>
|
|
9
20
|
<% end %>
|
|
10
21
|
</ul>
|
|
@@ -13,3 +24,6 @@
|
|
|
13
24
|
<% end %>
|
|
14
25
|
|
|
15
26
|
<p><%= link_to('New Project', plan_my_stuff.new_project_path) %></p>
|
|
27
|
+
<% if @support_user %>
|
|
28
|
+
<p><%= link_to('New Testing Project', plan_my_stuff.new_testing_project_path) %></p>
|
|
29
|
+
<% end %>
|