plan_my_stuff 0.1.0 → 1.0.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 +595 -0
- data/CONFIGURATION.md +487 -0
- data/README.md +612 -88
- data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
- data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
- data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
- data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
- data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
- data/app/jobs/plan_my_stuff/application_job.rb +8 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
- data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
- data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
- data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +43 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
- data/lib/plan_my_stuff/application_record.rb +158 -1
- data/lib/plan_my_stuff/approval.rb +88 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +12 -0
- data/lib/plan_my_stuff/attachment.rb +83 -0
- data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
- data/lib/plan_my_stuff/base_metadata.rb +25 -28
- data/lib/plan_my_stuff/base_project.rb +502 -0
- data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
- data/lib/plan_my_stuff/base_project_item.rb +588 -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 +139 -64
- data/lib/plan_my_stuff/comment.rb +225 -100
- data/lib/plan_my_stuff/comment_metadata.rb +68 -5
- data/lib/plan_my_stuff/configuration.rb +459 -28
- data/lib/plan_my_stuff/custom_fields.rb +96 -12
- data/lib/plan_my_stuff/engine.rb +14 -2
- data/lib/plan_my_stuff/errors.rb +65 -5
- data/lib/plan_my_stuff/graphql/queries.rb +454 -0
- data/lib/plan_my_stuff/issue.rb +1097 -166
- data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
- data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
- data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
- data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
- data/lib/plan_my_stuff/issue_field.rb +126 -0
- data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
- data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
- data/lib/plan_my_stuff/issue_metadata.rb +132 -21
- data/lib/plan_my_stuff/label.rb +100 -13
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/markdown.rb +13 -7
- data/lib/plan_my_stuff/metadata_parser.rb +51 -12
- data/lib/plan_my_stuff/notifications.rb +148 -0
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +40 -0
- data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
- data/lib/plan_my_stuff/pipeline.rb +310 -0
- data/lib/plan_my_stuff/project.rb +63 -465
- data/lib/plan_my_stuff/project_item.rb +3 -409
- 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 +12 -0
- data/lib/plan_my_stuff/repo.rb +145 -0
- data/lib/plan_my_stuff/test_helpers.rb +265 -25
- data/lib/plan_my_stuff/testing_project.rb +292 -0
- data/lib/plan_my_stuff/testing_project_item.rb +218 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +24 -3
- data/lib/plan_my_stuff/verifier.rb +10 -0
- data/lib/plan_my_stuff/version.rb +2 -2
- data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
- data/lib/plan_my_stuff.rb +55 -20
- data/lib/tasks/plan_my_stuff.rake +331 -0
- metadata +99 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Resolves linked issues from pull request payloads and finds their
|
|
6
|
+
# corresponding ProjectItems in the pipeline project.
|
|
7
|
+
#
|
|
8
|
+
# Uses +module_function+ pattern (matches +MetadataParser+, +Pipeline+).
|
|
9
|
+
#
|
|
10
|
+
module IssueLinker
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Regex matching GitHub closing keywords in PR bodies.
|
|
14
|
+
# Matches all nine variants: close/closes/closed, fix/fixes/fixed,
|
|
15
|
+
# resolve/resolves/resolved (case-insensitive).
|
|
16
|
+
CLOSING_KEYWORD_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/i
|
|
17
|
+
|
|
18
|
+
# Extracts issue numbers from a PR payload by parsing the PR body
|
|
19
|
+
# and commit messages for GitHub closing keywords (close/closes/closed,
|
|
20
|
+
# fix/fixes/fixed, resolve/resolves/resolved).
|
|
21
|
+
#
|
|
22
|
+
# @param pr_payload [Hash] parsed PR webhook payload (expects +:body+ or +"body"+ key)
|
|
23
|
+
# @param commit_messages [Array<String>] commit messages to scan (default: [])
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Integer>] referenced issue numbers (empty if none found)
|
|
26
|
+
#
|
|
27
|
+
def extract_issue_numbers(pr_payload, commit_messages: [])
|
|
28
|
+
texts = []
|
|
29
|
+
|
|
30
|
+
body = pr_payload[:body] || pr_payload['body']
|
|
31
|
+
texts << body if body.present?
|
|
32
|
+
|
|
33
|
+
Array.wrap(commit_messages).each do |message|
|
|
34
|
+
texts << message if message.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return [] if texts.empty?
|
|
38
|
+
|
|
39
|
+
matches = texts.join("\n").scan(CLOSING_KEYWORD_PATTERN).flatten
|
|
40
|
+
matches.map!(&:to_i)
|
|
41
|
+
matches.uniq!
|
|
42
|
+
matches
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Finds the ProjectItem in the pipeline project that matches the given
|
|
46
|
+
# issue number. Loads the full project and iterates its items.
|
|
47
|
+
#
|
|
48
|
+
# @param issue_number [Integer] issue number to find
|
|
49
|
+
# @param repo [Symbol, String, nil] unused, reserved for future multi-repo support
|
|
50
|
+
# @param project_number [Integer, nil] pipeline project number
|
|
51
|
+
#
|
|
52
|
+
# @return [PlanMyStuff::ProjectItem, nil] matching item, or nil if not found
|
|
53
|
+
#
|
|
54
|
+
def find_project_item(issue_number, _repo: nil, project_number: nil)
|
|
55
|
+
number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!(project_number)
|
|
56
|
+
project = PlanMyStuff::Project.find(number)
|
|
57
|
+
|
|
58
|
+
project.items.find { |item| item.number == issue_number }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Canonical pipeline status constants.
|
|
6
|
+
#
|
|
7
|
+
# Each constant holds the human-readable status name that maps to a
|
|
8
|
+
# GitHub Projects V2 single-select field value. Consuming apps can
|
|
9
|
+
# override display names via +PlanMyStuff.configuration.pipeline_statuses+
|
|
10
|
+
# but the canonical names here remain the internal identifiers.
|
|
11
|
+
#
|
|
12
|
+
module Status
|
|
13
|
+
STARTED = 'Started'
|
|
14
|
+
IN_REVIEW = 'In Review'
|
|
15
|
+
READY_FOR_RELEASE = 'Ready for Release'
|
|
16
|
+
RELEASE_IN_PROGRESS = 'Release in Progress'
|
|
17
|
+
COMPLETED = 'Completed'
|
|
18
|
+
|
|
19
|
+
# All statuses in pipeline order (frozen).
|
|
20
|
+
ALL = [
|
|
21
|
+
STARTED,
|
|
22
|
+
IN_REVIEW,
|
|
23
|
+
READY_FOR_RELEASE,
|
|
24
|
+
RELEASE_IN_PROGRESS,
|
|
25
|
+
COMPLETED,
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Converts a canonical status name to a snake_case key suitable for
|
|
29
|
+
# notification event names.
|
|
30
|
+
#
|
|
31
|
+
# @param canonical [String] one of the canonical status names
|
|
32
|
+
#
|
|
33
|
+
# @return [String] snake_case key (e.g. +"ready_for_release"+)
|
|
34
|
+
#
|
|
35
|
+
def self.key_for(canonical)
|
|
36
|
+
canonical.downcase.tr(' ', '_')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# High-level orchestration layer for the release pipeline.
|
|
5
|
+
#
|
|
6
|
+
# Named lifecycle methods wrap the low-level +ProjectItem.move_to!+ calls, resolve configurable status aliases, and
|
|
7
|
+
# fire +ActiveSupport::Notifications+ events on every transition.
|
|
8
|
+
#
|
|
9
|
+
# No transition enforcement -- any status can move to any other.
|
|
10
|
+
#
|
|
11
|
+
module Pipeline
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Raises +PlanMyStuff::PendingApprovalsError+ when +issue+ has pending manager approvals. No-op for +nil+ issues or
|
|
15
|
+
# issues that either have no approvers required or are fully approved.
|
|
16
|
+
#
|
|
17
|
+
# Called at the top of every forward transition (+take!+, +mark_in_review!+, +request_testing!+,
|
|
18
|
+
# +mark_ready_for_release!+). Batch / CI-driven transitions (+start_deployment!+, +complete_deployment!+) and
|
|
19
|
+
# reverse transitions (+remove!+) are intentionally NOT gated -- earlier forward transitions already required
|
|
20
|
+
# approval, and gating batch/automated paths would abort entire deploys on a single approval revoke.
|
|
21
|
+
#
|
|
22
|
+
# @raise [PlanMyStuff::PendingApprovalsError] when +issue+ has pending approvals
|
|
23
|
+
#
|
|
24
|
+
# @param issue [PlanMyStuff::Issue, nil]
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
27
|
+
#
|
|
28
|
+
def guard_approvals!(issue)
|
|
29
|
+
return if issue.nil?
|
|
30
|
+
|
|
31
|
+
return unless issue.approvals_required?
|
|
32
|
+
|
|
33
|
+
return if issue.fully_approved?
|
|
34
|
+
|
|
35
|
+
raise(PlanMyStuff::PendingApprovalsError.new(
|
|
36
|
+
issue: issue,
|
|
37
|
+
pending_count: issue.pending_approvals.count,
|
|
38
|
+
))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resolves a canonical status name to its configured display alias, falling back to the canonical name when no
|
|
42
|
+
# alias is configured.
|
|
43
|
+
#
|
|
44
|
+
# @param canonical [String] one of the +Pipeline::Status+ constants
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
47
|
+
#
|
|
48
|
+
def resolve_status_name(canonical)
|
|
49
|
+
PlanMyStuff.configuration.pipeline_statuses.fetch(canonical, canonical)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fires a +pipeline_<event>.plan_my_stuff+ notification.
|
|
53
|
+
#
|
|
54
|
+
# When +event+ is a canonical +Pipeline::Status+ name, the event key is derived via +Status.key_for+ and the
|
|
55
|
+
# canonical status is added to the payload as +:status+. Otherwise +event+ is used verbatim as the suffix (e.g.
|
|
56
|
+
# +"removed"+, +"removed_late"+, +"testing"+).
|
|
57
|
+
#
|
|
58
|
+
# +resource+ is normally a single project item (payload carries +:issue_number+). Pass an +Array+ for a batch
|
|
59
|
+
# event (e.g. the deployment-completed sweep) and the payload instead carries +:issue_numbers+ -- the linked
|
|
60
|
+
# number of every item.
|
|
61
|
+
#
|
|
62
|
+
# @param event [String] status name or literal event suffix
|
|
63
|
+
# @param resource [PlanMyStuff::BaseProjectItem, Array<PlanMyStuff::BaseProjectItem>] item or batch of items
|
|
64
|
+
# @param extra [Hash] additional payload entries
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
#
|
|
68
|
+
def instrument(event, resource, **extra)
|
|
69
|
+
extra_to_use = { **extra }
|
|
70
|
+
event_to_use = event
|
|
71
|
+
if PlanMyStuff::Pipeline::Status::ALL.include?(event_to_use)
|
|
72
|
+
extra_to_use = { status: event, **extra_to_use }
|
|
73
|
+
event_to_use = PlanMyStuff::Pipeline::Status.key_for(event_to_use)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
number_fields =
|
|
77
|
+
if resource.is_a?(Array)
|
|
78
|
+
{ issue_numbers: resource.map(&:number) }
|
|
79
|
+
else
|
|
80
|
+
{ issue_number: resource.number }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
PlanMyStuff::Notifications.instrument(
|
|
84
|
+
"pipeline_#{event_to_use}",
|
|
85
|
+
resource,
|
|
86
|
+
**number_fields,
|
|
87
|
+
**extra_to_use,
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the pipeline project number from the explicit argument, +pipeline_project_number+, or
|
|
92
|
+
# +default_project_number+.
|
|
93
|
+
#
|
|
94
|
+
# @raise [ArgumentError] if no project number can be resolved
|
|
95
|
+
#
|
|
96
|
+
# @param project_number [Integer, nil]
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer]
|
|
99
|
+
#
|
|
100
|
+
def resolve_pipeline_project_number!(project_number = nil)
|
|
101
|
+
config = PlanMyStuff.configuration
|
|
102
|
+
result = project_number || config.pipeline_project_number || config.default_project_number
|
|
103
|
+
|
|
104
|
+
raise(ArgumentError, 'No pipeline project number configured') if result.nil?
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Removes a project item from the pipeline project entirely. Captures the prior status before deletion so
|
|
110
|
+
# subscribers can decide whether the removal happened late in the lifecycle.
|
|
111
|
+
#
|
|
112
|
+
# Always fires +pipeline_removed.plan_my_stuff+. Additionally fires +pipeline_removed_late.plan_my_stuff+ when
|
|
113
|
+
# +prior_status+ is past "Started" (i.e. anything other than "Started", accounting for configured status aliases).
|
|
114
|
+
# +nil+ status is treated as not-late.
|
|
115
|
+
#
|
|
116
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
117
|
+
#
|
|
118
|
+
# @return [String, nil] the deleted item ID, or nil if already destroyed
|
|
119
|
+
#
|
|
120
|
+
def remove!(project_item)
|
|
121
|
+
prior_status = project_item.status
|
|
122
|
+
result = project_item.destroy!
|
|
123
|
+
|
|
124
|
+
instrument('removed', project_item, prior_status: prior_status)
|
|
125
|
+
instrument('removed_late', project_item, prior_status: prior_status) if late_removal?(prior_status)
|
|
126
|
+
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns true when +prior_status+ is past "Started" in the pipeline. Honors configured status aliases. +nil+ is
|
|
131
|
+
# treated as not-late.
|
|
132
|
+
#
|
|
133
|
+
# @param prior_status [String, nil]
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean]
|
|
136
|
+
#
|
|
137
|
+
def late_removal?(prior_status)
|
|
138
|
+
return false if prior_status.nil?
|
|
139
|
+
|
|
140
|
+
started = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED)
|
|
141
|
+
prior_status != started
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns true when +project_item+ is at a release-cycle status ("Ready for Release", "Release in Progress", or
|
|
145
|
+
# "Completed"). Honors configured status aliases.
|
|
146
|
+
#
|
|
147
|
+
# Used to lock items against webhook-driven removals once they enter the release path -- a stray unassign should
|
|
148
|
+
# not yank an item out of "Release in Progress".
|
|
149
|
+
#
|
|
150
|
+
# @param project_item [PlanMyStuff::BaseProjectItem]
|
|
151
|
+
#
|
|
152
|
+
# @return [Boolean]
|
|
153
|
+
#
|
|
154
|
+
def release_cycle_locked?(project_item)
|
|
155
|
+
locked_statuses = [
|
|
156
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE),
|
|
157
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS),
|
|
158
|
+
resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
locked_statuses.include?(project_item.status)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Moves a project item to "Started". When +user+ is a support user, also stamps +metadata.responded_at+ on the
|
|
165
|
+
# issue via +Issue#mark_responded!+ (a no-op if it's already responded to / not a PMS issue).
|
|
166
|
+
#
|
|
167
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
168
|
+
# @param user [Object, nil] actor forwarded to the +pipeline_started.plan_my_stuff+ payload
|
|
169
|
+
#
|
|
170
|
+
# @return [Hash] mutation result
|
|
171
|
+
#
|
|
172
|
+
def take!(project_item, user: nil)
|
|
173
|
+
guard_approvals!(project_item&.issue)
|
|
174
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED)
|
|
175
|
+
result = project_item.move_to!(status)
|
|
176
|
+
|
|
177
|
+
project_item.issue.mark_responded!(user) if user.present?
|
|
178
|
+
|
|
179
|
+
instrument(PlanMyStuff::Pipeline::Status::STARTED, project_item, user: user)
|
|
180
|
+
result
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Moves a project item to "In Review".
|
|
184
|
+
#
|
|
185
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
186
|
+
#
|
|
187
|
+
# @return [Hash] mutation result
|
|
188
|
+
#
|
|
189
|
+
def mark_in_review!(project_item)
|
|
190
|
+
guard_approvals!(project_item&.issue)
|
|
191
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::IN_REVIEW)
|
|
192
|
+
result = project_item.move_to!(status)
|
|
193
|
+
|
|
194
|
+
instrument(PlanMyStuff::Pipeline::Status::IN_REVIEW, project_item)
|
|
195
|
+
result
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Marks a project item as in testing by setting the +Testing+ custom single-select field to its active value. Does
|
|
199
|
+
# NOT change the item's +Status+ -- testing runs orthogonally to +In Review+.
|
|
200
|
+
#
|
|
201
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
202
|
+
# @param user [Object, nil] actor forwarded to the +pipeline_testing.plan_my_stuff+ payload
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash] mutation result
|
|
205
|
+
#
|
|
206
|
+
def request_testing!(project_item, user: nil)
|
|
207
|
+
guard_approvals!(project_item&.issue)
|
|
208
|
+
field_name = PlanMyStuff.configuration.pipeline_testing_field_name
|
|
209
|
+
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active)
|
|
210
|
+
result = project_item.update_single_select_field!(field_name, value)
|
|
211
|
+
|
|
212
|
+
instrument('testing', project_item, field_name: field_name, value: value, user: user)
|
|
213
|
+
|
|
214
|
+
result
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Reverses +request_testing!+ by flipping the +Testing+ custom single-select field back to its inactive value.
|
|
218
|
+
# Not approval-gated -- the forward transition (+request_testing!+) was already gated, so clearing should always
|
|
219
|
+
# succeed (matches +remove!+).
|
|
220
|
+
#
|
|
221
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
222
|
+
# @param user [Object, nil] actor forwarded to the +pipeline_testing_cleared.plan_my_stuff+ payload
|
|
223
|
+
#
|
|
224
|
+
# @return [Hash] mutation result
|
|
225
|
+
#
|
|
226
|
+
def clear_testing!(project_item, user: nil)
|
|
227
|
+
field_name = PlanMyStuff.configuration.pipeline_testing_field_name
|
|
228
|
+
value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:inactive)
|
|
229
|
+
result = project_item.update_single_select_field!(field_name, value)
|
|
230
|
+
|
|
231
|
+
instrument('testing_cleared', project_item, field_name: field_name, value: value, user: user)
|
|
232
|
+
|
|
233
|
+
result
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Moves a project item to "Ready for Release".
|
|
237
|
+
#
|
|
238
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
239
|
+
#
|
|
240
|
+
# @return [Hash] mutation result
|
|
241
|
+
#
|
|
242
|
+
def mark_ready_for_release!(project_item)
|
|
243
|
+
guard_approvals!(project_item&.issue)
|
|
244
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE)
|
|
245
|
+
result = project_item.move_to!(status)
|
|
246
|
+
|
|
247
|
+
instrument(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE, project_item)
|
|
248
|
+
result
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Finds ALL items at "Ready for Release" in the pipeline project and moves each to "Release in Progress". Fires an
|
|
252
|
+
# event per item.
|
|
253
|
+
#
|
|
254
|
+
# When +commit_sha+ is given (the merge_commit_sha of the PR into +production+), it is stamped onto every moved
|
|
255
|
+
# item's linked issue metadata. +AwsController#handle_deployment_completed+ later matches this sha against the
|
|
256
|
+
# configured +production_commit_sha+ to decide which items to auto-complete, so this is the only commit sha in the
|
|
257
|
+
# release cycle that matters.
|
|
258
|
+
#
|
|
259
|
+
# @param project_number [Integer, nil]
|
|
260
|
+
# @param commit_sha [String, nil]
|
|
261
|
+
#
|
|
262
|
+
# @return [Array<PlanMyStuff::ProjectItem>]
|
|
263
|
+
#
|
|
264
|
+
def start_deployment!(project_number: nil, commit_sha: nil)
|
|
265
|
+
number = resolve_pipeline_project_number!(project_number)
|
|
266
|
+
project = PlanMyStuff::Project.find(number)
|
|
267
|
+
|
|
268
|
+
ready_status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE)
|
|
269
|
+
in_progress_status = resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS)
|
|
270
|
+
|
|
271
|
+
items = project.items.select { |item| item.status == ready_status }
|
|
272
|
+
|
|
273
|
+
items.each do |item|
|
|
274
|
+
if commit_sha.present?
|
|
275
|
+
issue = item.issue
|
|
276
|
+
issue.metadata.commit_sha = commit_sha
|
|
277
|
+
issue.save!
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
item.move_to!(in_progress_status)
|
|
281
|
+
instrument(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS, item, commit_sha: commit_sha)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
items
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Moves a project item to "Completed" if the linked issue has +auto_complete+ enabled, and sets the linked issue's
|
|
288
|
+
# "Issue Status" field to "Fixed". Returns +nil+ when auto-complete is off (item stays at "Release in Progress" and
|
|
289
|
+
# no field is touched). The "Issue Status" update is skipped when +config.issue_fields_enabled+ is +false+ so
|
|
290
|
+
# completion still succeeds.
|
|
291
|
+
#
|
|
292
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
293
|
+
# @param deployment_id [String, nil]
|
|
294
|
+
#
|
|
295
|
+
# @return [Hash, nil] mutation result or nil
|
|
296
|
+
#
|
|
297
|
+
def complete_deployment!(project_item, deployment_id: nil)
|
|
298
|
+
issue = project_item.issue
|
|
299
|
+
return unless issue.metadata.auto_complete?
|
|
300
|
+
|
|
301
|
+
status = resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED)
|
|
302
|
+
result = project_item.move_to!(status)
|
|
303
|
+
|
|
304
|
+
issue.set_issue_fields!('Issue Status' => 'Fixed') if PlanMyStuff.configuration.issue_fields_enabled
|
|
305
|
+
|
|
306
|
+
instrument(PlanMyStuff::Pipeline::Status::COMPLETED, project_item, deployment_id: deployment_id)
|
|
307
|
+
result
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|