plan_my_stuff 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +569 -38
- data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
- data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
- data/app/controllers/plan_my_stuff/projects_controller.rb +65 -1
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
- data/app/jobs/plan_my_stuff/application_job.rb +9 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
- data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +17 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +11 -4
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +38 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +138 -5
- data/lib/plan_my_stuff/application_record.rb +144 -0
- data/lib/plan_my_stuff/approval.rb +80 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +14 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
- data/lib/plan_my_stuff/base_metadata.rb +0 -11
- data/lib/plan_my_stuff/base_project.rb +661 -0
- data/lib/plan_my_stuff/base_project_item.rb +562 -0
- data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
- data/lib/plan_my_stuff/cache.rb +197 -0
- data/lib/plan_my_stuff/client.rb +7 -0
- data/lib/plan_my_stuff/comment.rb +174 -54
- data/lib/plan_my_stuff/configuration.rb +254 -8
- data/lib/plan_my_stuff/custom_fields.rb +31 -17
- data/lib/plan_my_stuff/engine.rb +0 -4
- data/lib/plan_my_stuff/errors.rb +49 -0
- data/lib/plan_my_stuff/graphql/queries.rb +392 -0
- data/lib/plan_my_stuff/issue.rb +1477 -174
- data/lib/plan_my_stuff/issue_metadata.rb +122 -0
- data/lib/plan_my_stuff/label.rb +82 -11
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/notifications.rb +142 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +44 -0
- data/lib/plan_my_stuff/pipeline.rb +293 -0
- data/lib/plan_my_stuff/project.rb +62 -468
- data/lib/plan_my_stuff/project_item.rb +3 -417
- data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
- data/lib/plan_my_stuff/project_metadata.rb +47 -0
- data/lib/plan_my_stuff/reminders/closer.rb +70 -0
- data/lib/plan_my_stuff/reminders/fire.rb +129 -0
- data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
- data/lib/plan_my_stuff/reminders.rb +16 -0
- data/lib/plan_my_stuff/test_helpers.rb +260 -15
- data/lib/plan_my_stuff/testing_project.rb +291 -0
- data/lib/plan_my_stuff/testing_project_item.rb +184 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +8 -3
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
- data/lib/plan_my_stuff.rb +16 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +54 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/notifications'
|
|
4
|
+
require_relative 'pipeline/issue_linker'
|
|
5
|
+
require_relative 'pipeline/status'
|
|
6
|
+
|
|
7
|
+
module PlanMyStuff
|
|
8
|
+
# High-level orchestration layer for the release pipeline.
|
|
9
|
+
#
|
|
10
|
+
# Named lifecycle methods wrap the low-level +ProjectItem.move_to!+ calls,
|
|
11
|
+
# resolve configurable status aliases, and fire
|
|
12
|
+
# +ActiveSupport::Notifications+ events on every transition.
|
|
13
|
+
#
|
|
14
|
+
# No transition enforcement -- any status can move to any other.
|
|
15
|
+
#
|
|
16
|
+
module Pipeline
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Raises +PlanMyStuff::PendingApprovalsError+ when +issue+ has
|
|
20
|
+
# pending manager approvals. No-op for +nil+ issues or issues that
|
|
21
|
+
# either have no approvers required or are fully approved.
|
|
22
|
+
#
|
|
23
|
+
# Called at the top of every forward transition (+submit!+, +take!+,
|
|
24
|
+
# +mark_in_review!+, +request_testing!+,
|
|
25
|
+
# +mark_ready_for_release!+). Batch / CI-driven transitions
|
|
26
|
+
# (+start_deployment!+, +complete_deployment!+) and reverse
|
|
27
|
+
# transitions (+remove!+) are intentionally NOT gated -- earlier
|
|
28
|
+
# forward transitions already required approval, and gating
|
|
29
|
+
# batch/automated paths would abort entire deploys on a single
|
|
30
|
+
# approval revoke.
|
|
31
|
+
#
|
|
32
|
+
# @param issue [PlanMyStuff::Issue, nil]
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
#
|
|
36
|
+
def guard_approvals!(issue)
|
|
37
|
+
return if issue.nil?
|
|
38
|
+
|
|
39
|
+
return unless issue.approvals_required?
|
|
40
|
+
|
|
41
|
+
return if issue.fully_approved?
|
|
42
|
+
|
|
43
|
+
raise(PlanMyStuff::PendingApprovalsError.new(
|
|
44
|
+
issue: issue,
|
|
45
|
+
pending_count: issue.pending_approvals.count,
|
|
46
|
+
))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Resolves a canonical status name to its configured display alias,
|
|
50
|
+
# falling back to the canonical name when no alias is configured.
|
|
51
|
+
#
|
|
52
|
+
# @param canonical [String] one of the +Pipeline::Status+ constants
|
|
53
|
+
#
|
|
54
|
+
# @return [String]
|
|
55
|
+
#
|
|
56
|
+
def resolve_status_name(canonical)
|
|
57
|
+
PlanMyStuff.configuration.pipeline_statuses.fetch(canonical, canonical)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Fires an +ActiveSupport::Notifications+ event for a pipeline transition.
|
|
61
|
+
#
|
|
62
|
+
# @param status [String] canonical status name (used for event key)
|
|
63
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
64
|
+
# @param extra [Hash] additional payload entries
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
#
|
|
68
|
+
def instrument(status, project_item, **extra)
|
|
69
|
+
key = PlanMyStuff::Pipeline::Status.key_for(status)
|
|
70
|
+
payload = {
|
|
71
|
+
project_item: project_item,
|
|
72
|
+
issue_number: project_item.number,
|
|
73
|
+
status: status,
|
|
74
|
+
timestamp: Time.current,
|
|
75
|
+
}.merge(extra)
|
|
76
|
+
|
|
77
|
+
ActiveSupport::Notifications.instrument("plan_my_stuff.pipeline.#{key}", payload)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the pipeline project number from the explicit argument,
|
|
81
|
+
# +pipeline_project_number+, or +default_project_number+.
|
|
82
|
+
#
|
|
83
|
+
# @param project_number [Integer, nil]
|
|
84
|
+
#
|
|
85
|
+
# @return [Integer]
|
|
86
|
+
#
|
|
87
|
+
# @raise [ArgumentError] if no project number can be resolved
|
|
88
|
+
#
|
|
89
|
+
def resolve_pipeline_project_number(project_number = nil)
|
|
90
|
+
config = PlanMyStuff.configuration
|
|
91
|
+
result = project_number || config.pipeline_project_number || config.default_project_number
|
|
92
|
+
|
|
93
|
+
raise(ArgumentError, 'No pipeline project number configured') if result.nil?
|
|
94
|
+
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Adds an issue to the pipeline project, moves it to "Submitted",
|
|
99
|
+
# and assigns the given developer.
|
|
100
|
+
#
|
|
101
|
+
# Called when an issue is assigned to a dev, NOT on issue creation.
|
|
102
|
+
# Un-triaged issues should not be in the pipeline.
|
|
103
|
+
#
|
|
104
|
+
# @param issue [PlanMyStuff::Issue]
|
|
105
|
+
# @param assignee [String] GitHub username
|
|
106
|
+
# @param project_number [Integer, nil]
|
|
107
|
+
#
|
|
108
|
+
# @return [PlanMyStuff::ProjectItem]
|
|
109
|
+
#
|
|
110
|
+
def submit!(issue, assignee:, project_number: nil)
|
|
111
|
+
guard_approvals!(issue)
|
|
112
|
+
number = resolve_pipeline_project_number(project_number)
|
|
113
|
+
project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
|
|
114
|
+
|
|
115
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::SUBMITTED)
|
|
116
|
+
project_item.move_to!(status)
|
|
117
|
+
project_item.assign!(assignee)
|
|
118
|
+
|
|
119
|
+
instrument(PlanMyStuff::Pipeline::Status::SUBMITTED, project_item)
|
|
120
|
+
project_item
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Removes a project item from the pipeline project entirely.
|
|
124
|
+
# Captures the prior status before deletion so subscribers can
|
|
125
|
+
# decide whether the removal happened late in the lifecycle.
|
|
126
|
+
#
|
|
127
|
+
# Always fires +plan_my_stuff.pipeline.removed+. Additionally fires
|
|
128
|
+
# +plan_my_stuff.pipeline.removed_late+ when +prior_status+ is past
|
|
129
|
+
# "Started" (i.e. anything other than "Submitted" or "Started",
|
|
130
|
+
# accounting for configured status aliases). +nil+ status is treated
|
|
131
|
+
# as not-late.
|
|
132
|
+
#
|
|
133
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
134
|
+
#
|
|
135
|
+
# @return [String, nil] the deleted item ID, or nil if already destroyed
|
|
136
|
+
#
|
|
137
|
+
def remove!(project_item)
|
|
138
|
+
prior_status = project_item.status
|
|
139
|
+
result = project_item.destroy!
|
|
140
|
+
|
|
141
|
+
payload = {
|
|
142
|
+
project_item: project_item,
|
|
143
|
+
issue_number: project_item.number,
|
|
144
|
+
prior_status: prior_status,
|
|
145
|
+
timestamp: Time.current,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed', payload)
|
|
149
|
+
|
|
150
|
+
if late_removal?(prior_status)
|
|
151
|
+
ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed_late', payload)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
result
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns true when +prior_status+ is past "Started" in the pipeline.
|
|
158
|
+
# Honors configured status aliases. +nil+ is treated as not-late.
|
|
159
|
+
#
|
|
160
|
+
# @param prior_status [String, nil]
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
#
|
|
164
|
+
def late_removal?(prior_status)
|
|
165
|
+
return false if prior_status.nil?
|
|
166
|
+
|
|
167
|
+
early_statuses = [
|
|
168
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::SUBMITTED),
|
|
169
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
early_statuses.exclude?(prior_status)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Moves a project item to "Started".
|
|
176
|
+
#
|
|
177
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash] mutation result
|
|
180
|
+
#
|
|
181
|
+
def take!(project_item)
|
|
182
|
+
guard_approvals!(project_item&.issue)
|
|
183
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED)
|
|
184
|
+
result = project_item.move_to!(status)
|
|
185
|
+
|
|
186
|
+
instrument(PlanMyStuff::Pipeline::Status::STARTED, project_item)
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Moves a project item to "In Review".
|
|
191
|
+
#
|
|
192
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
193
|
+
#
|
|
194
|
+
# @return [Hash] mutation result
|
|
195
|
+
#
|
|
196
|
+
def mark_in_review!(project_item)
|
|
197
|
+
guard_approvals!(project_item&.issue)
|
|
198
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::IN_REVIEW)
|
|
199
|
+
result = project_item.move_to!(status)
|
|
200
|
+
|
|
201
|
+
instrument(PlanMyStuff::Pipeline::Status::IN_REVIEW, project_item)
|
|
202
|
+
result
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Moves a project item to "Testing".
|
|
206
|
+
#
|
|
207
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
208
|
+
#
|
|
209
|
+
# @return [Hash] mutation result
|
|
210
|
+
#
|
|
211
|
+
def request_testing!(project_item)
|
|
212
|
+
guard_approvals!(project_item&.issue)
|
|
213
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::TESTING)
|
|
214
|
+
result = project_item.move_to!(status)
|
|
215
|
+
|
|
216
|
+
instrument(PlanMyStuff::Pipeline::Status::TESTING, project_item)
|
|
217
|
+
result
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Moves a project item to "Ready for Release".
|
|
221
|
+
#
|
|
222
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
223
|
+
#
|
|
224
|
+
# @return [Hash] mutation result
|
|
225
|
+
#
|
|
226
|
+
def mark_ready_for_release!(project_item)
|
|
227
|
+
guard_approvals!(project_item&.issue)
|
|
228
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE)
|
|
229
|
+
result = project_item.move_to!(status)
|
|
230
|
+
|
|
231
|
+
instrument(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE, project_item)
|
|
232
|
+
result
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Finds ALL items at "Ready for Release" in the pipeline project and
|
|
236
|
+
# moves each to "Release in Progress". Fires an event per item.
|
|
237
|
+
#
|
|
238
|
+
# When +commit_sha+ is given (the merge_commit_sha of the PR into
|
|
239
|
+
# +production+), it is stamped onto every moved item's linked issue
|
|
240
|
+
# metadata. +AwsController#handle_deployment_completed+ later matches
|
|
241
|
+
# this sha against the configured +production_commit_sha+ to decide
|
|
242
|
+
# which items to auto-complete, so this is the only commit sha in the
|
|
243
|
+
# release cycle that matters.
|
|
244
|
+
#
|
|
245
|
+
# @param project_number [Integer, nil]
|
|
246
|
+
# @param commit_sha [String, nil]
|
|
247
|
+
#
|
|
248
|
+
# @return [Array<PlanMyStuff::ProjectItem>]
|
|
249
|
+
#
|
|
250
|
+
def start_deployment!(project_number: nil, commit_sha: nil)
|
|
251
|
+
number = resolve_pipeline_project_number(project_number)
|
|
252
|
+
project = PlanMyStuff::Project.find(number)
|
|
253
|
+
|
|
254
|
+
ready_status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE)
|
|
255
|
+
in_progress_status = resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS)
|
|
256
|
+
|
|
257
|
+
items = project.items.select { |item| item.status == ready_status }
|
|
258
|
+
|
|
259
|
+
items.each do |item|
|
|
260
|
+
if commit_sha.present?
|
|
261
|
+
issue = item.issue
|
|
262
|
+
issue.metadata.commit_sha = commit_sha
|
|
263
|
+
issue.save!
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
item.move_to!(in_progress_status)
|
|
267
|
+
instrument(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS, item, commit_sha: commit_sha)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
items
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Moves a project item to "Completed" if the linked issue has
|
|
274
|
+
# +auto_complete+ enabled. Returns +nil+ when auto-complete is off
|
|
275
|
+
# (item stays at "Release in Progress").
|
|
276
|
+
#
|
|
277
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
278
|
+
# @param deployment_id [String, nil]
|
|
279
|
+
#
|
|
280
|
+
# @return [Hash, nil] mutation result or nil
|
|
281
|
+
#
|
|
282
|
+
def complete_deployment!(project_item, deployment_id: nil)
|
|
283
|
+
issue = project_item.issue
|
|
284
|
+
return unless issue.metadata.auto_complete?
|
|
285
|
+
|
|
286
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED)
|
|
287
|
+
result = project_item.move_to!(status)
|
|
288
|
+
|
|
289
|
+
instrument(PlanMyStuff::Pipeline::Status::COMPLETED, project_item, deployment_id: deployment_id)
|
|
290
|
+
result
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|