plan_my_stuff 0.5.0 → 0.6.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 +48 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +10 -1
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +105 -11
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +1 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +1 -1
- data/lib/plan_my_stuff/base_project.rb +1 -0
- data/lib/plan_my_stuff/base_project_item.rb +18 -0
- data/lib/plan_my_stuff/configuration.rb +35 -1
- data/lib/plan_my_stuff/graphql/queries.rb +1 -0
- data/lib/plan_my_stuff/issue.rb +8 -0
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
- data/lib/plan_my_stuff/pipeline/status.rb +0 -4
- data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
- data/lib/plan_my_stuff/pipeline.rb +42 -37
- data/lib/plan_my_stuff/test_helpers.rb +10 -33
- data/lib/plan_my_stuff/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69661ace546ff5f775e0e9fb5e4a153c3aca9e68da0b568e85238d8b66ef620a
|
|
4
|
+
data.tar.gz: e0698c727fe6c3f1e8e135d0a2b1860731188dc01496065e3622a0a1d6ef0c45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 876ac7c9c1baf371e09d81535324ae9a35bcd1c3a30509931395e274a229fcf25397ba1377450cb6a8ad6465f0474174ea664579b59f0d407c9b83ee08523647
|
|
7
|
+
data.tar.gz: 89c4a1865f38c614c05d802c02d267b0ff2e83339da936403fac08f3d98476a6b87b722eecf422b5ee8f101f5db1ce75e8d3f836a3f54dd0ca86713df3146feb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- `Pipeline.submit!` removed; consuming apps that called it directly should switch to `ProjectItem.create!` + `Pipeline.take!`
|
|
8
|
+
- `Pipeline::Status::SUBMITTED` and `Pipeline::Status::TESTING` constants removed; `Status::ALL` no longer includes them
|
|
9
|
+
- `Pipeline.request_testing!` no longer moves the `Status` field — it now writes to a separate `Testing` single-select custom field on the pipeline project. The pipeline project must have a `Testing` field with `Testing` / `Not testing` options
|
|
10
|
+
- Active pipeline status set is now `Started → In Review → Ready for Release → Release in Progress → Completed`
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Pipeline::Testing` module (`FIELD_NAME`, `VALUES`)
|
|
15
|
+
- `Pipeline::CompletedSweep` removes items from `Completed` after the configured TTL; driven by `RemindersSweepJob`
|
|
16
|
+
- `BaseProjectItem#update_single_select_field!` instance method
|
|
17
|
+
- `BaseProjectItem#updated_at` populated from GitHub's `updatedAt`
|
|
18
|
+
- `Issue#assignees` public reader
|
|
19
|
+
- `Pipeline.release_cycle_locked?` helper
|
|
20
|
+
- New config: `pipeline_testing_field_name`, `pipeline_testing_values`, `pipeline_completion_purge_enabled` (default `true`), `pipeline_completion_ttl_hours` (default `24`)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Assigning a dev to an issue now creates the project item and lands it at `Started` (was `Submitted`)
|
|
25
|
+
- Opening a PR as a draft adds the linked issue to the pipeline at `Started` and assigns the PR author when the issue has no assignees; already-pipelined items are not moved
|
|
26
|
+
- The assignment webhook no longer calls `assign!` on the project item — GitHub already records the assignment, and `assign!` could clobber co-assignees
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- `Pipeline.submit!` (see Breaking)
|
|
31
|
+
- `Pipeline::Status::SUBMITTED`, `Pipeline::Status::TESTING` (see Breaking)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Closed issues no longer alter the pipeline on assign/unassign webhook events
|
|
36
|
+
- Items at `Ready for Release`, `Release in Progress`, or `Completed` are no longer removed when an unassign webhook fires
|
|
37
|
+
- Pending-approvals guard runs before `ProjectItem.create!` in the assignment flow so the webhook no longer leaves orphan items when `Pipeline.take!`'s approval guard would otherwise raise
|
|
38
|
+
|
|
39
|
+
### Documented
|
|
40
|
+
|
|
41
|
+
- `designs/pipeline_cleanup/plan.md` — full design + bug-to-fix mapping
|
|
42
|
+
- `requirements/08_release_pipeline.md` updated to reflect the new flow
|
|
43
|
+
- `requirements/tasks.md` — T-PC-001 added under Phase 14
|
|
44
|
+
|
|
45
|
+
## 0.5.1
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- AWS webhook rejected SNS `SubscriptionConfirmation` and `UnsubscribeConfirmation` messages with 401; now signature-verified, logged, and acked with 200
|
|
50
|
+
|
|
3
51
|
## 0.5.0
|
|
4
52
|
|
|
5
53
|
### Added
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
module Webhooks
|
|
5
5
|
class AwsController < ActionController::API
|
|
6
|
-
VALID_SNS_MESSAGE_TYPES = %w[Notification].freeze
|
|
6
|
+
VALID_SNS_MESSAGE_TYPES = %w[Notification SubscriptionConfirmation UnsubscribeConfirmation].freeze
|
|
7
7
|
|
|
8
8
|
before_action :verify_signature!
|
|
9
9
|
|
|
10
10
|
# POST /webhooks/aws
|
|
11
11
|
def create
|
|
12
12
|
config = PlanMyStuff.configuration
|
|
13
|
+
message_type = request.headers['x-amz-sns-message-type'].to_s
|
|
14
|
+
|
|
15
|
+
Rails.logger.info("[PlanMyStuff] SNS #{message_type}: #{sns_params.to_unsafe_h.inspect}")
|
|
16
|
+
|
|
17
|
+
unless message_type == 'Notification'
|
|
18
|
+
head(:ok)
|
|
19
|
+
|
|
20
|
+
return
|
|
21
|
+
end
|
|
13
22
|
|
|
14
23
|
unless config.process_aws_webhooks
|
|
15
24
|
head(:ok)
|
|
@@ -81,14 +81,27 @@ module PlanMyStuff
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Adds the issue to the pipeline project at "
|
|
84
|
+
# Adds the issue to the pipeline project at "Started" the first
|
|
85
85
|
# time it is assigned. Re-assigns and additional assignees are
|
|
86
86
|
# ignored -- if the issue is already in the pipeline we do not
|
|
87
87
|
# touch its status.
|
|
88
88
|
#
|
|
89
|
+
# Skipped paths (no project item is created):
|
|
90
|
+
# - Issue is closed (assignment changes on a closed issue
|
|
91
|
+
# must not alter the pipeline)
|
|
92
|
+
# - Issue has pending approvals (creating + +take!+ would
|
|
93
|
+
# either orphan an item or 500 on the approval guard)
|
|
94
|
+
#
|
|
95
|
+
# GitHub already records the issue assignment (that's what
|
|
96
|
+
# triggered this webhook), so the gem does not call +assign!+
|
|
97
|
+
# on the project item -- that would clobber co-assignees on
|
|
98
|
+
# the underlying issue.
|
|
99
|
+
#
|
|
89
100
|
# @return [void]
|
|
90
101
|
#
|
|
91
102
|
def handle_issue_assigned
|
|
103
|
+
return if issue_params[:state] == 'closed'
|
|
104
|
+
|
|
92
105
|
issue_number = issue_params.fetch(:number)
|
|
93
106
|
assignee_login = payload_params.dig(:assignee, :login)
|
|
94
107
|
|
|
@@ -96,14 +109,23 @@ module PlanMyStuff
|
|
|
96
109
|
|
|
97
110
|
existing = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
98
111
|
if existing.present?
|
|
99
|
-
Rails.logger.info("[PlanMyStuff] Issue ##{issue_number} already in pipeline project, skipping
|
|
112
|
+
Rails.logger.info("[PlanMyStuff] Issue ##{issue_number} already in pipeline project, skipping")
|
|
100
113
|
|
|
101
114
|
return
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
repo = payload_params.dig(:repository, :full_name)
|
|
105
118
|
issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
|
|
106
|
-
|
|
119
|
+
|
|
120
|
+
if issue.approvals_required? && !issue.fully_approved?
|
|
121
|
+
Rails.logger.info("[PlanMyStuff] Issue ##{issue_number} has pending approvals, skipping")
|
|
122
|
+
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
|
|
127
|
+
project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
|
|
128
|
+
PlanMyStuff::Pipeline.take!(project_item)
|
|
107
129
|
end
|
|
108
130
|
|
|
109
131
|
# Removes the issue from the pipeline project when the LAST
|
|
@@ -111,9 +133,17 @@ module PlanMyStuff
|
|
|
111
133
|
# a no-op (only one of N was removed). If the issue isn't in
|
|
112
134
|
# the pipeline at all, also a no-op (logged at info).
|
|
113
135
|
#
|
|
136
|
+
# Closed issues are skipped: assignment changes on a closed
|
|
137
|
+
# issue must not alter the pipeline. Items already on the
|
|
138
|
+
# release cycle (Ready for Release, Release in Progress,
|
|
139
|
+
# Completed) are also locked -- once on the release path an
|
|
140
|
+
# item should not come off via webhook.
|
|
141
|
+
#
|
|
114
142
|
# @return [void]
|
|
115
143
|
#
|
|
116
144
|
def handle_issue_unassigned
|
|
145
|
+
return if issue_params[:state] == 'closed'
|
|
146
|
+
|
|
117
147
|
remaining_assignees = Array.wrap(issue_params[:assignees])
|
|
118
148
|
return if remaining_assignees.any?
|
|
119
149
|
|
|
@@ -128,6 +158,14 @@ module PlanMyStuff
|
|
|
128
158
|
return
|
|
129
159
|
end
|
|
130
160
|
|
|
161
|
+
if PlanMyStuff::Pipeline.release_cycle_locked?(project_item)
|
|
162
|
+
Rails.logger.info(
|
|
163
|
+
"[PlanMyStuff] Issue ##{issue_number} on release cycle (#{project_item.status}), skipping remove!",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
131
169
|
PlanMyStuff::Pipeline.remove!(project_item)
|
|
132
170
|
end
|
|
133
171
|
|
|
@@ -205,7 +243,7 @@ module PlanMyStuff
|
|
|
205
243
|
|
|
206
244
|
case action
|
|
207
245
|
when 'opened'
|
|
208
|
-
pull_request_params[:draft] ?
|
|
246
|
+
pull_request_params[:draft] ? handle_draft_opened : handle_ready_for_review
|
|
209
247
|
when 'ready_for_review', 'reopened'
|
|
210
248
|
handle_ready_for_review
|
|
211
249
|
when 'converted_to_draft'
|
|
@@ -215,6 +253,29 @@ module PlanMyStuff
|
|
|
215
253
|
end
|
|
216
254
|
end
|
|
217
255
|
|
|
256
|
+
# Opening a PR as a draft is a soft "I've started working on this"
|
|
257
|
+
# signal. For each linked issue, when the issue is not yet in the
|
|
258
|
+
# pipeline, add it at "Started"; when the issue has no assignees,
|
|
259
|
+
# assign the PR author. Already-in-pipeline items are NOT moved
|
|
260
|
+
# (a draft open is not a status change).
|
|
261
|
+
#
|
|
262
|
+
# @return [void]
|
|
263
|
+
#
|
|
264
|
+
def handle_draft_opened
|
|
265
|
+
repo = payload_params.dig(:repository, :full_name)
|
|
266
|
+
pr_author = pull_request_params.dig(:user, :login)
|
|
267
|
+
|
|
268
|
+
each_linked_issue_number do |issue_number|
|
|
269
|
+
issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
|
|
270
|
+
|
|
271
|
+
if PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number).nil?
|
|
272
|
+
ensure_in_pipeline_at_started(issue)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
assign_pr_author(issue, pr_author) if pr_author.present? && issue.assignees.empty?
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
218
279
|
# @return [void]
|
|
219
280
|
def handle_ready_for_review
|
|
220
281
|
each_linked_item do |item|
|
|
@@ -222,6 +283,34 @@ module PlanMyStuff
|
|
|
222
283
|
end
|
|
223
284
|
end
|
|
224
285
|
|
|
286
|
+
# Adds the issue to the pipeline at "Started". Skipped when the
|
|
287
|
+
# issue has pending approvals (Pipeline.take!'s guard would
|
|
288
|
+
# otherwise leave an orphan project item behind).
|
|
289
|
+
#
|
|
290
|
+
# @param issue [PlanMyStuff::Issue]
|
|
291
|
+
#
|
|
292
|
+
# @return [void]
|
|
293
|
+
#
|
|
294
|
+
def ensure_in_pipeline_at_started(issue)
|
|
295
|
+
if issue.approvals_required? && !issue.fully_approved?
|
|
296
|
+
Rails.logger.info("[PlanMyStuff] Issue ##{issue.number} has pending approvals, skipping pipeline add")
|
|
297
|
+
return
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
|
|
301
|
+
project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
|
|
302
|
+
PlanMyStuff::Pipeline.take!(project_item)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# @param issue [PlanMyStuff::Issue]
|
|
306
|
+
# @param pr_author [String]
|
|
307
|
+
#
|
|
308
|
+
# @return [void]
|
|
309
|
+
#
|
|
310
|
+
def assign_pr_author(issue, pr_author)
|
|
311
|
+
PlanMyStuff::Issue.update!(number: issue.number, repo: issue.repo, assignees: [pr_author])
|
|
312
|
+
end
|
|
313
|
+
|
|
225
314
|
# @return [void]
|
|
226
315
|
def handle_converted_to_draft
|
|
227
316
|
each_linked_item do |item|
|
|
@@ -247,13 +336,7 @@ module PlanMyStuff
|
|
|
247
336
|
|
|
248
337
|
# @yield [PlanMyStuff::ProjectItem]
|
|
249
338
|
def each_linked_item
|
|
250
|
-
|
|
251
|
-
numbers = PlanMyStuff::Pipeline::IssueLinker.extract_issue_numbers(
|
|
252
|
-
pull_request_params,
|
|
253
|
-
commit_messages: messages,
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
numbers.each do |issue_number|
|
|
339
|
+
each_linked_issue_number do |issue_number|
|
|
257
340
|
item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
258
341
|
|
|
259
342
|
if item
|
|
@@ -264,6 +347,17 @@ module PlanMyStuff
|
|
|
264
347
|
end
|
|
265
348
|
end
|
|
266
349
|
|
|
350
|
+
# @yield [Integer] linked issue number
|
|
351
|
+
def each_linked_issue_number(&)
|
|
352
|
+
messages = fetch_commit_messages
|
|
353
|
+
numbers = PlanMyStuff::Pipeline::IssueLinker.extract_issue_numbers(
|
|
354
|
+
pull_request_params,
|
|
355
|
+
commit_messages: messages,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
numbers.each(&)
|
|
359
|
+
end
|
|
360
|
+
|
|
267
361
|
# Fetches commit messages for the current PR via the GitHub API.
|
|
268
362
|
#
|
|
269
363
|
# @return [Array<String>] commit messages (empty on failure)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<%= button_to('Mark waiting', plan_my_stuff.issue_waiting_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
|
|
18
18
|
<% end %>
|
|
19
19
|
<% end %>
|
|
20
|
-
<% if @support_user && @pipeline_enabled &&
|
|
20
|
+
<% if @support_user && @pipeline_enabled && @pipeline_item.nil? %>
|
|
21
21
|
<%= button_to('Take', plan_my_stuff.issue_take_path(@issue.number, repo: @issue.repo.full_name), method: :post) %>
|
|
22
22
|
<% end %>
|
|
23
23
|
<% if @support_user %>
|
|
@@ -331,6 +331,7 @@ module PlanMyStuff
|
|
|
331
331
|
repo: repo_name.present? ? PlanMyStuff::Repo.resolve(repo_name) : nil,
|
|
332
332
|
status: extract_item_status(field_values),
|
|
333
333
|
field_values: parse_field_values(field_values),
|
|
334
|
+
updated_at: item[:updatedAt],
|
|
334
335
|
github_response: item,
|
|
335
336
|
}
|
|
336
337
|
end
|
|
@@ -43,6 +43,8 @@ module PlanMyStuff
|
|
|
43
43
|
attribute :state, :string
|
|
44
44
|
# @return [String, nil]
|
|
45
45
|
attribute :status, :string
|
|
46
|
+
# @return [Time, nil] GitHub's updatedAt timestamp
|
|
47
|
+
attribute :updated_at, :datetime
|
|
46
48
|
# @return [Hash]
|
|
47
49
|
attribute :field_values, default: -> { {} }
|
|
48
50
|
# @return [PlanMyStuff::BaseProject, nil]
|
|
@@ -477,6 +479,22 @@ module PlanMyStuff
|
|
|
477
479
|
)
|
|
478
480
|
end
|
|
479
481
|
|
|
482
|
+
# Updates a single-select custom field on this item.
|
|
483
|
+
#
|
|
484
|
+
# @param field_name [String] single-select field name (e.g. "Testing")
|
|
485
|
+
# @param value [String] option name to select
|
|
486
|
+
#
|
|
487
|
+
# @return [Hash] mutation result
|
|
488
|
+
#
|
|
489
|
+
def update_single_select_field!(field_name, value)
|
|
490
|
+
self.class.update_single_select_field!(
|
|
491
|
+
project_number: project.number,
|
|
492
|
+
item_id: id,
|
|
493
|
+
field_name: field_name,
|
|
494
|
+
value: value,
|
|
495
|
+
)
|
|
496
|
+
end
|
|
497
|
+
|
|
480
498
|
# Deletes this item from its parent project. Marks the in-memory
|
|
481
499
|
# instance as destroyed so +destroyed?+ returns true and +persisted?+
|
|
482
500
|
# returns false.
|
|
@@ -164,12 +164,27 @@ module PlanMyStuff
|
|
|
164
164
|
attr_accessor :production_commit_sha
|
|
165
165
|
|
|
166
166
|
# Canonical status name to display alias map. Allows consuming apps to rename
|
|
167
|
-
# pipeline statuses (e.g. "
|
|
167
|
+
# pipeline statuses (e.g. "Started" to "In Progress").
|
|
168
168
|
#
|
|
169
169
|
# @return [Hash{String => String}]
|
|
170
170
|
#
|
|
171
171
|
attr_accessor :pipeline_statuses
|
|
172
172
|
|
|
173
|
+
# Display name for the +Testing+ single-select custom field on the
|
|
174
|
+
# pipeline project. Defaults to +"Testing"+.
|
|
175
|
+
#
|
|
176
|
+
# @return [String]
|
|
177
|
+
#
|
|
178
|
+
attr_accessor :pipeline_testing_field_name
|
|
179
|
+
|
|
180
|
+
# Map of canonical testing field option keys (+:active+, +:inactive+)
|
|
181
|
+
# to display labels. Allows consuming apps to rename the option labels
|
|
182
|
+
# without changing the canonical identifiers.
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash{Symbol => String}]
|
|
185
|
+
#
|
|
186
|
+
attr_accessor :pipeline_testing_values
|
|
187
|
+
|
|
173
188
|
# @return [String] branch name that PRs merge into for "Ready for release" transition
|
|
174
189
|
attr_accessor :main_branch
|
|
175
190
|
|
|
@@ -281,6 +296,21 @@ module PlanMyStuff
|
|
|
281
296
|
#
|
|
282
297
|
attr_accessor :archived_label
|
|
283
298
|
|
|
299
|
+
# Whether the pipeline sweep removes aged-out +Completed+ items.
|
|
300
|
+
# Defaults to +true+. Set to +false+ to keep items in +Completed+
|
|
301
|
+
# indefinitely.
|
|
302
|
+
#
|
|
303
|
+
# @return [Boolean]
|
|
304
|
+
#
|
|
305
|
+
attr_accessor :pipeline_completion_purge_enabled
|
|
306
|
+
|
|
307
|
+
# Hours after a project item's last update at which the sweep removes
|
|
308
|
+
# it from the pipeline if its status is +Completed+. Defaults to +24+.
|
|
309
|
+
#
|
|
310
|
+
# @return [Integer]
|
|
311
|
+
#
|
|
312
|
+
attr_accessor :pipeline_completion_ttl_hours
|
|
313
|
+
|
|
284
314
|
# Whether to process incoming AWS webhook events. Defaults to +Rails.env.production?+.
|
|
285
315
|
#
|
|
286
316
|
# @return [Boolean]
|
|
@@ -323,6 +353,8 @@ module PlanMyStuff
|
|
|
323
353
|
@testing_custom_fields = {}
|
|
324
354
|
@pipeline_enabled = true
|
|
325
355
|
@pipeline_statuses = {}
|
|
356
|
+
@pipeline_testing_field_name = PlanMyStuff::Pipeline::Testing::FIELD_NAME
|
|
357
|
+
@pipeline_testing_values = PlanMyStuff::Pipeline::Testing::VALUES.dup
|
|
326
358
|
@main_branch = 'main'
|
|
327
359
|
@production_branch = 'production'
|
|
328
360
|
@mount_groups = { webhooks: true, issues: true, projects: true }
|
|
@@ -338,6 +370,8 @@ module PlanMyStuff
|
|
|
338
370
|
@archiving_enabled = true
|
|
339
371
|
@archive_closed_after_days = 90
|
|
340
372
|
@archived_label = 'archived'
|
|
373
|
+
@pipeline_completion_purge_enabled = true
|
|
374
|
+
@pipeline_completion_ttl_hours = 24
|
|
341
375
|
@process_aws_webhooks = Rails.env.production?
|
|
342
376
|
@sns_verifier_class = ::Aws::SNS::MessageVerifier if defined?(::Aws::SNS::MessageVerifier)
|
|
343
377
|
@sns_verifier_error =
|
data/lib/plan_my_stuff/issue.rb
CHANGED
|
@@ -689,6 +689,14 @@ module PlanMyStuff
|
|
|
689
689
|
safe_read_field(github_response, :html_url)
|
|
690
690
|
end
|
|
691
691
|
|
|
692
|
+
# GitHub assignees for this issue, by login.
|
|
693
|
+
#
|
|
694
|
+
# @return [Array<String>]
|
|
695
|
+
#
|
|
696
|
+
def assignees
|
|
697
|
+
extract_assignee_logins(github_response)
|
|
698
|
+
end
|
|
699
|
+
|
|
692
700
|
# @return [Boolean]
|
|
693
701
|
def pms_issue?
|
|
694
702
|
metadata.schema_version.present?
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Removes pipeline project items that have been at +Completed+ for
|
|
6
|
+
# longer than the configured TTL.
|
|
7
|
+
#
|
|
8
|
+
# Driven by +RemindersSweepJob+ so the gem ships one scheduled
|
|
9
|
+
# entrypoint. Items are removed via +Pipeline.remove!+ so subscribers
|
|
10
|
+
# see the standard +plan_my_stuff.pipeline.removed+ event.
|
|
11
|
+
#
|
|
12
|
+
module CompletedSweep
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Runs the sweep. No-op when
|
|
16
|
+
# +configuration.pipeline_completion_purge_enabled+ is false.
|
|
17
|
+
#
|
|
18
|
+
# @param project_number [Integer, nil]
|
|
19
|
+
# @param now [Time]
|
|
20
|
+
#
|
|
21
|
+
# @return [Array<PlanMyStuff::ProjectItem>] removed items
|
|
22
|
+
#
|
|
23
|
+
def perform(project_number: nil, now: Time.current)
|
|
24
|
+
config = PlanMyStuff.configuration
|
|
25
|
+
return [] unless config.pipeline_completion_purge_enabled
|
|
26
|
+
|
|
27
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number(project_number)
|
|
28
|
+
project = PlanMyStuff::Project.find(number)
|
|
29
|
+
|
|
30
|
+
completed_status = PlanMyStuff::Pipeline.resolve_status_name(
|
|
31
|
+
PlanMyStuff::Pipeline::Status::COMPLETED,
|
|
32
|
+
)
|
|
33
|
+
ttl = config.pipeline_completion_ttl_hours.hours
|
|
34
|
+
|
|
35
|
+
candidates = project.items.select do |item|
|
|
36
|
+
item.status == completed_status &&
|
|
37
|
+
item.updated_at.present? &&
|
|
38
|
+
item.updated_at + ttl < now
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
candidates.each { |item| PlanMyStuff::Pipeline.remove!(item) }
|
|
42
|
+
candidates
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -10,20 +10,16 @@ module PlanMyStuff
|
|
|
10
10
|
# but the canonical names here remain the internal identifiers.
|
|
11
11
|
#
|
|
12
12
|
module Status
|
|
13
|
-
SUBMITTED = 'Submitted'
|
|
14
13
|
STARTED = 'Started'
|
|
15
14
|
IN_REVIEW = 'In Review'
|
|
16
|
-
TESTING = 'Testing'
|
|
17
15
|
READY_FOR_RELEASE = 'Ready for Release'
|
|
18
16
|
RELEASE_IN_PROGRESS = 'Release in Progress'
|
|
19
17
|
COMPLETED = 'Completed'
|
|
20
18
|
|
|
21
19
|
# All statuses in pipeline order (frozen).
|
|
22
20
|
ALL = [
|
|
23
|
-
SUBMITTED,
|
|
24
21
|
STARTED,
|
|
25
22
|
IN_REVIEW,
|
|
26
|
-
TESTING,
|
|
27
23
|
READY_FOR_RELEASE,
|
|
28
24
|
RELEASE_IN_PROGRESS,
|
|
29
25
|
COMPLETED,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Canonical names for the +Testing+ single-select custom field on the
|
|
6
|
+
# pipeline project. The field tracks testing progress orthogonally to
|
|
7
|
+
# the +Status+ field (testing can run alongside +In Review+).
|
|
8
|
+
#
|
|
9
|
+
# Consuming apps can override the field name via
|
|
10
|
+
# +PlanMyStuff.configuration.pipeline_testing_field_name+ and the
|
|
11
|
+
# option labels via +PlanMyStuff.configuration.pipeline_testing_values+.
|
|
12
|
+
# The canonical names here remain the internal identifiers.
|
|
13
|
+
#
|
|
14
|
+
module Testing
|
|
15
|
+
FIELD_NAME = 'Testing'
|
|
16
|
+
|
|
17
|
+
VALUES = {
|
|
18
|
+
active: 'Testing',
|
|
19
|
+
inactive: 'Not testing',
|
|
20
|
+
}.freeze
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_support/notifications'
|
|
4
|
+
require_relative 'pipeline/completed_sweep'
|
|
4
5
|
require_relative 'pipeline/issue_linker'
|
|
5
6
|
require_relative 'pipeline/status'
|
|
7
|
+
require_relative 'pipeline/testing'
|
|
6
8
|
|
|
7
9
|
module PlanMyStuff
|
|
8
10
|
# High-level orchestration layer for the release pipeline.
|
|
@@ -20,7 +22,7 @@ module PlanMyStuff
|
|
|
20
22
|
# pending manager approvals. No-op for +nil+ issues or issues that
|
|
21
23
|
# either have no approvers required or are fully approved.
|
|
22
24
|
#
|
|
23
|
-
# Called at the top of every forward transition (+
|
|
25
|
+
# Called at the top of every forward transition (+take!+,
|
|
24
26
|
# +mark_in_review!+, +request_testing!+,
|
|
25
27
|
# +mark_ready_for_release!+). Batch / CI-driven transitions
|
|
26
28
|
# (+start_deployment!+, +complete_deployment!+) and reverse
|
|
@@ -95,40 +97,14 @@ module PlanMyStuff
|
|
|
95
97
|
result
|
|
96
98
|
end
|
|
97
99
|
|
|
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
100
|
# Removes a project item from the pipeline project entirely.
|
|
124
101
|
# Captures the prior status before deletion so subscribers can
|
|
125
102
|
# decide whether the removal happened late in the lifecycle.
|
|
126
103
|
#
|
|
127
104
|
# Always fires +plan_my_stuff.pipeline.removed+. Additionally fires
|
|
128
105
|
# +plan_my_stuff.pipeline.removed_late+ when +prior_status+ is past
|
|
129
|
-
# "Started" (i.e. anything other than "
|
|
130
|
-
#
|
|
131
|
-
# as not-late.
|
|
106
|
+
# "Started" (i.e. anything other than "Started", accounting for
|
|
107
|
+
# configured status aliases). +nil+ status is treated as not-late.
|
|
132
108
|
#
|
|
133
109
|
# @param project_item [PlanMyStuff::ProjectItem]
|
|
134
110
|
#
|
|
@@ -164,12 +140,30 @@ module PlanMyStuff
|
|
|
164
140
|
def late_removal?(prior_status)
|
|
165
141
|
return false if prior_status.nil?
|
|
166
142
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
143
|
+
started = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED)
|
|
144
|
+
prior_status != started
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns true when +project_item+ is at a release-cycle status
|
|
148
|
+
# ("Ready for Release", "Release in Progress", or "Completed").
|
|
149
|
+
# Honors configured status aliases.
|
|
150
|
+
#
|
|
151
|
+
# Used to lock items against webhook-driven removals once they
|
|
152
|
+
# enter the release path -- a stray unassign should not yank an
|
|
153
|
+
# item out of "Release in Progress".
|
|
154
|
+
#
|
|
155
|
+
# @param project_item [PlanMyStuff::BaseProjectItem]
|
|
156
|
+
#
|
|
157
|
+
# @return [Boolean]
|
|
158
|
+
#
|
|
159
|
+
def release_cycle_locked?(project_item)
|
|
160
|
+
locked_statuses = [
|
|
161
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE),
|
|
162
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS),
|
|
163
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED),
|
|
170
164
|
]
|
|
171
165
|
|
|
172
|
-
|
|
166
|
+
locked_statuses.include?(project_item.status)
|
|
173
167
|
end
|
|
174
168
|
|
|
175
169
|
# Moves a project item to "Started".
|
|
@@ -202,7 +196,9 @@ module PlanMyStuff
|
|
|
202
196
|
result
|
|
203
197
|
end
|
|
204
198
|
|
|
205
|
-
#
|
|
199
|
+
# Marks a project item as in testing by setting the +Testing+ custom
|
|
200
|
+
# single-select field to its active value. Does NOT change the item's
|
|
201
|
+
# +Status+ -- testing runs orthogonally to +In Review+.
|
|
206
202
|
#
|
|
207
203
|
# @param project_item [PlanMyStuff::ProjectItem]
|
|
208
204
|
#
|
|
@@ -210,10 +206,19 @@ module PlanMyStuff
|
|
|
210
206
|
#
|
|
211
207
|
def request_testing!(project_item)
|
|
212
208
|
guard_approvals!(project_item&.issue)
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
field_name = PlanMyStuff.configuration.pipeline_testing_field_name
|
|
210
|
+
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active)
|
|
211
|
+
result = project_item.update_single_select_field!(field_name, value)
|
|
212
|
+
|
|
213
|
+
ActiveSupport::Notifications.instrument(
|
|
214
|
+
'plan_my_stuff.pipeline.testing',
|
|
215
|
+
project_item: project_item,
|
|
216
|
+
issue_number: project_item.number,
|
|
217
|
+
field_name: field_name,
|
|
218
|
+
value: value,
|
|
219
|
+
timestamp: Time.current,
|
|
220
|
+
)
|
|
215
221
|
|
|
216
|
-
instrument(PlanMyStuff::Pipeline::Status::TESTING, project_item)
|
|
217
222
|
result
|
|
218
223
|
end
|
|
219
224
|
|
|
@@ -266,11 +266,6 @@ module PlanMyStuff
|
|
|
266
266
|
expect_pms_action(:item_assigned, 'item to be assigned', **filters)
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
-
# @param filters [Hash] attribute filters (e.g. assignee:, project_number:)
|
|
270
|
-
def expect_pms_pipeline_submitted(**filters)
|
|
271
|
-
expect_pms_action(:pipeline_submitted, 'pipeline submission', **filters)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
269
|
# @param filters [Hash] attribute filters (e.g. item_id:)
|
|
275
270
|
def expect_pms_pipeline_taken(**filters)
|
|
276
271
|
expect_pms_action(:pipeline_started, 'pipeline item to be taken', **filters)
|
|
@@ -612,36 +607,9 @@ module PlanMyStuff
|
|
|
612
607
|
def stub_pipeline_methods!
|
|
613
608
|
pipeline_mod = PlanMyStuff::Pipeline
|
|
614
609
|
%i[
|
|
615
|
-
|
|
610
|
+
take! mark_in_review! request_testing! mark_ready_for_release! start_deployment! complete_deployment!
|
|
616
611
|
].each { |m| save_original(pipeline_mod, m) }
|
|
617
612
|
|
|
618
|
-
pipeline_mod.define_singleton_method(:submit!) do |issue, assignee:, project_number: nil|
|
|
619
|
-
number =
|
|
620
|
-
project_number || PlanMyStuff.configuration.pipeline_project_number ||
|
|
621
|
-
PlanMyStuff.configuration.default_project_number
|
|
622
|
-
|
|
623
|
-
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
624
|
-
type: :pipeline_submitted,
|
|
625
|
-
params: {
|
|
626
|
-
issue_number: issue.respond_to?(:number) ? issue.number : nil,
|
|
627
|
-
assignee: assignee,
|
|
628
|
-
project_number: number,
|
|
629
|
-
},
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
project = PlanMyStuff::TestHelpers.build_project(number: number || 1)
|
|
633
|
-
PlanMyStuff::ProjectItem.build(
|
|
634
|
-
{
|
|
635
|
-
id: "PVTI_fake_#{rand(10_000)}",
|
|
636
|
-
title: issue.respond_to?(:title) ? issue.title : 'Untitled',
|
|
637
|
-
number: issue.respond_to?(:number) ? issue.number : nil,
|
|
638
|
-
status: PlanMyStuff::Pipeline::Status::SUBMITTED,
|
|
639
|
-
field_values: {},
|
|
640
|
-
},
|
|
641
|
-
project: project,
|
|
642
|
-
)
|
|
643
|
-
end
|
|
644
|
-
|
|
645
613
|
pipeline_mod.define_singleton_method(:take!) do |project_item|
|
|
646
614
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
647
615
|
type: :pipeline_started,
|
|
@@ -667,14 +635,23 @@ module PlanMyStuff
|
|
|
667
635
|
end
|
|
668
636
|
|
|
669
637
|
pipeline_mod.define_singleton_method(:request_testing!) do |project_item|
|
|
638
|
+
field_name = PlanMyStuff.configuration.pipeline_testing_field_name
|
|
639
|
+
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active)
|
|
640
|
+
|
|
670
641
|
PlanMyStuff::TestHelpers.recorded_actions << {
|
|
671
642
|
type: :pipeline_testing,
|
|
672
643
|
params: {
|
|
673
644
|
item_id: project_item.respond_to?(:id) ? project_item.id : nil,
|
|
674
645
|
issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
|
|
646
|
+
field_name: field_name,
|
|
647
|
+
value: value,
|
|
675
648
|
},
|
|
676
649
|
}
|
|
677
650
|
|
|
651
|
+
if project_item.respond_to?(:field_values) && project_item.field_values.is_a?(Hash)
|
|
652
|
+
project_item.field_values[field_name] = value
|
|
653
|
+
end
|
|
654
|
+
|
|
678
655
|
nil
|
|
679
656
|
end
|
|
680
657
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plan_my_stuff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brands Insurance
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -130,8 +130,10 @@ files:
|
|
|
130
130
|
- lib/plan_my_stuff/metadata_parser.rb
|
|
131
131
|
- lib/plan_my_stuff/notifications.rb
|
|
132
132
|
- lib/plan_my_stuff/pipeline.rb
|
|
133
|
+
- lib/plan_my_stuff/pipeline/completed_sweep.rb
|
|
133
134
|
- lib/plan_my_stuff/pipeline/issue_linker.rb
|
|
134
135
|
- lib/plan_my_stuff/pipeline/status.rb
|
|
136
|
+
- lib/plan_my_stuff/pipeline/testing.rb
|
|
135
137
|
- lib/plan_my_stuff/project.rb
|
|
136
138
|
- lib/plan_my_stuff/project_item.rb
|
|
137
139
|
- lib/plan_my_stuff/project_item_metadata.rb
|