Module: PlanMyStuff::Pipeline
- Defined in:
- lib/plan_my_stuff/pipeline.rb,
lib/plan_my_stuff/pipeline/status.rb,
lib/plan_my_stuff/pipeline/testing.rb,
lib/plan_my_stuff/pipeline/issue_linker.rb,
lib/plan_my_stuff/pipeline/completed_sweep.rb
Overview
High-level orchestration layer for the release pipeline.
Named lifecycle methods wrap the low-level ProjectItem.move_to! calls, resolve configurable status aliases, and fire ActiveSupport::Notifications events on every transition.
No transition enforcement – any status can move to any other.
Defined Under Namespace
Modules: CompletedSweep, IssueLinker, Status, Testing
Class Method Summary collapse
-
.clear_testing!(project_item, user: nil) ⇒ Hash
Reverses
request_testing!by flipping theTestingcustom single-select field back to its inactive value. -
.complete_deployment!(project_item, deployment_id: nil) ⇒ Hash?
Moves a project item to “Completed” if the linked issue has
auto_completeenabled, and sets the linked issue’s “Issue Status” field to “Fixed”. -
.guard_approvals!(issue) ⇒ void
Raises
PlanMyStuff::PendingApprovalsErrorwhenissuehas pending manager approvals. -
.instrument(event, resource, **extra) ⇒ void
Fires a pipeline_<event>.plan_my_stuff notification.
-
.late_removal?(prior_status) ⇒ Boolean
Returns true when
prior_statusis past “Started” in the pipeline. -
.mark_in_review!(project_item) ⇒ Hash
Moves a project item to “In Review”.
-
.mark_ready_for_release!(project_item) ⇒ Hash
Moves a project item to “Ready for Release”.
-
.release_cycle_locked?(project_item) ⇒ Boolean
Returns true when
project_itemis at a release-cycle status (“Ready for Release”, “Release in Progress”, or “Completed”). -
.remove!(project_item) ⇒ String?
Removes a project item from the pipeline project entirely.
-
.request_testing!(project_item, user: nil) ⇒ Hash
Marks a project item as in testing by setting the
Testingcustom single-select field to its active value. -
.resolve_pipeline_project_number!(project_number = nil) ⇒ Integer
Returns the pipeline project number from the explicit argument,
pipeline_project_number, ordefault_project_number. -
.resolve_status_name(canonical) ⇒ String
Resolves a canonical status name to its configured display alias, falling back to the canonical name when no alias is configured.
-
.start_deployment!(project_number: nil, commit_sha: nil) ⇒ Array<PlanMyStuff::ProjectItem>
Finds ALL items at “Ready for Release” in the pipeline project and moves each to “Release in Progress”.
-
.take!(project_item, user: nil) ⇒ Hash
Moves a project item to “Started”.
Class Method Details
.clear_testing!(project_item, user: nil) ⇒ Hash
Reverses request_testing! by flipping the Testing custom single-select field back to its inactive value. Not approval-gated – the forward transition (request_testing!) was already gated, so clearing should always succeed (matches remove!).
226 227 228 229 230 231 232 233 234 |
# File 'lib/plan_my_stuff/pipeline.rb', line 226 def clear_testing!(project_item, user: nil) field_name = PlanMyStuff.configuration.pipeline_testing_field_name value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:inactive) result = project_item.update_single_select_field!(field_name, value) instrument('testing_cleared', project_item, field_name: field_name, value: value, user: user) result end |
.complete_deployment!(project_item, deployment_id: nil) ⇒ Hash?
Moves a project item to “Completed” if the linked issue has auto_complete enabled, and sets the linked issue’s “Issue Status” field to “Fixed”. Returns nil when auto-complete is off (item stays at “Release in Progress” and no field is touched). The “Issue Status” update is skipped when config.issue_fields_enabled is false so completion still succeeds.
297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/plan_my_stuff/pipeline.rb', line 297 def complete_deployment!(project_item, deployment_id: nil) issue = project_item.issue return unless issue..auto_complete? status = resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED) result = project_item.move_to!(status) issue.set_issue_fields!('Issue Status' => 'Fixed') if PlanMyStuff.configuration.issue_fields_enabled instrument(PlanMyStuff::Pipeline::Status::COMPLETED, project_item, deployment_id: deployment_id) result end |
.guard_approvals!(issue) ⇒ void
This method returns an undefined value.
Raises PlanMyStuff::PendingApprovalsError when issue has pending manager approvals. No-op for nil issues or issues that either have no approvers required or are fully approved.
Called at the top of every forward transition (take!, mark_in_review!, request_testing!, mark_ready_for_release!). Batch / CI-driven transitions (start_deployment!, complete_deployment!) and reverse transitions (remove!) are intentionally NOT gated – earlier forward transitions already required approval, and gating batch/automated paths would abort entire deploys on a single approval revoke.
28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/plan_my_stuff/pipeline.rb', line 28 def guard_approvals!(issue) return if issue.nil? return unless issue.approvals_required? return if issue.fully_approved? raise(PlanMyStuff::PendingApprovalsError.new( issue: issue, pending_count: issue.pending_approvals.count, )) end |
.instrument(event, resource, **extra) ⇒ void
This method returns an undefined value.
Fires a pipeline_<event>.plan_my_stuff notification.
When event is a canonical Pipeline::Status name, the event key is derived via Status.key_for and the canonical status is added to the payload as :status. Otherwise event is used verbatim as the suffix (e.g. “removed”, “removed_late”, “testing”).
resource is normally a single project item (payload carries :issue_number). Pass an Array for a batch event (e.g. the deployment-completed sweep) and the payload instead carries :issue_numbers – the linked number of every item.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/plan_my_stuff/pipeline.rb', line 68 def instrument(event, resource, **extra) extra_to_use = { **extra } event_to_use = event if PlanMyStuff::Pipeline::Status::ALL.include?(event_to_use) extra_to_use = { status: event, **extra_to_use } event_to_use = PlanMyStuff::Pipeline::Status.key_for(event_to_use) end number_fields = if resource.is_a?(Array) { issue_numbers: resource.map(&:number) } else { issue_number: resource.number } end PlanMyStuff::Notifications.instrument( "pipeline_#{event_to_use}", resource, **number_fields, **extra_to_use, ) end |
.late_removal?(prior_status) ⇒ Boolean
Returns true when prior_status is past “Started” in the pipeline. Honors configured status aliases. nil is treated as not-late.
137 138 139 140 141 142 |
# File 'lib/plan_my_stuff/pipeline.rb', line 137 def late_removal?(prior_status) return false if prior_status.nil? started = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED) prior_status != started end |
.mark_in_review!(project_item) ⇒ Hash
Moves a project item to “In Review”.
189 190 191 192 193 194 195 196 |
# File 'lib/plan_my_stuff/pipeline.rb', line 189 def mark_in_review!(project_item) guard_approvals!(project_item&.issue) status = resolve_status_name(PlanMyStuff::Pipeline::Status::IN_REVIEW) result = project_item.move_to!(status) instrument(PlanMyStuff::Pipeline::Status::IN_REVIEW, project_item) result end |
.mark_ready_for_release!(project_item) ⇒ Hash
Moves a project item to “Ready for Release”.
242 243 244 245 246 247 248 249 |
# File 'lib/plan_my_stuff/pipeline.rb', line 242 def mark_ready_for_release!(project_item) guard_approvals!(project_item&.issue) status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE) result = project_item.move_to!(status) instrument(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE, project_item) result end |
.release_cycle_locked?(project_item) ⇒ Boolean
Returns true when project_item is at a release-cycle status (“Ready for Release”, “Release in Progress”, or “Completed”). Honors configured status aliases.
Used to lock items against webhook-driven removals once they enter the release path – a stray unassign should not yank an item out of “Release in Progress”.
154 155 156 157 158 159 160 161 162 |
# File 'lib/plan_my_stuff/pipeline.rb', line 154 def release_cycle_locked?(project_item) locked_statuses = [ resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE), resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS), resolve_status_name(PlanMyStuff::Pipeline::Status::COMPLETED), ] locked_statuses.include?(project_item.status) end |
.remove!(project_item) ⇒ String?
Removes a project item from the pipeline project entirely. Captures the prior status before deletion so subscribers can decide whether the removal happened late in the lifecycle.
Always fires pipeline_removed.plan_my_stuff. Additionally fires pipeline_removed_late.plan_my_stuff when prior_status is past “Started” (i.e. anything other than “Started”, accounting for configured status aliases). nil status is treated as not-late.
120 121 122 123 124 125 126 127 128 |
# File 'lib/plan_my_stuff/pipeline.rb', line 120 def remove!(project_item) prior_status = project_item.status result = project_item.destroy! instrument('removed', project_item, prior_status: prior_status) instrument('removed_late', project_item, prior_status: prior_status) if late_removal?(prior_status) result end |
.request_testing!(project_item, user: nil) ⇒ Hash
Marks a project item as in testing by setting the Testing custom single-select field to its active value. Does NOT change the item’s Status – testing runs orthogonally to In Review.
206 207 208 209 210 211 212 213 214 215 |
# File 'lib/plan_my_stuff/pipeline.rb', line 206 def request_testing!(project_item, user: nil) guard_approvals!(project_item&.issue) field_name = PlanMyStuff.configuration.pipeline_testing_field_name value = PlanMyStuff.configuration.pipeline_testing_values.fetch(:active) result = project_item.update_single_select_field!(field_name, value) instrument('testing', project_item, field_name: field_name, value: value, user: user) result end |
.resolve_pipeline_project_number!(project_number = nil) ⇒ Integer
Returns the pipeline project number from the explicit argument, pipeline_project_number, or default_project_number.
100 101 102 103 104 105 106 107 |
# File 'lib/plan_my_stuff/pipeline.rb', line 100 def resolve_pipeline_project_number!(project_number = nil) config = PlanMyStuff.configuration result = project_number || config.pipeline_project_number || config.default_project_number raise(ArgumentError, 'No pipeline project number configured') if result.nil? result end |
.resolve_status_name(canonical) ⇒ String
Resolves a canonical status name to its configured display alias, falling back to the canonical name when no alias is configured.
48 49 50 |
# File 'lib/plan_my_stuff/pipeline.rb', line 48 def resolve_status_name(canonical) PlanMyStuff.configuration.pipeline_statuses.fetch(canonical, canonical) end |
.start_deployment!(project_number: nil, commit_sha: nil) ⇒ Array<PlanMyStuff::ProjectItem>
Finds ALL items at “Ready for Release” in the pipeline project and moves each to “Release in Progress”. Fires an event per item.
When commit_sha is given (the merge_commit_sha of the PR into production), it is stamped onto every moved item’s linked issue metadata. AwsController#handle_deployment_completed later matches this sha against the configured production_commit_sha to decide which items to auto-complete, so this is the only commit sha in the release cycle that matters.
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/plan_my_stuff/pipeline.rb', line 264 def start_deployment!(project_number: nil, commit_sha: nil) number = resolve_pipeline_project_number!(project_number) project = PlanMyStuff::Project.find(number) ready_status = resolve_status_name(PlanMyStuff::Pipeline::Status::READY_FOR_RELEASE) in_progress_status = resolve_status_name(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS) items = project.items.select { |item| item.status == ready_status } items.each do |item| if commit_sha.present? issue = item.issue issue..commit_sha = commit_sha issue.save! end item.move_to!(in_progress_status) instrument(PlanMyStuff::Pipeline::Status::RELEASE_IN_PROGRESS, item, commit_sha: commit_sha) end items end |
.take!(project_item, user: nil) ⇒ Hash
Moves a project item to “Started”. When user is a support user, also stamps metadata.responded_at on the issue via Issue#mark_responded! (a no-op if it’s already responded to / not a PMS issue).
172 173 174 175 176 177 178 179 180 181 |
# File 'lib/plan_my_stuff/pipeline.rb', line 172 def take!(project_item, user: nil) guard_approvals!(project_item&.issue) status = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED) result = project_item.move_to!(status) project_item.issue.mark_responded!(user) if user.present? instrument(PlanMyStuff::Pipeline::Status::STARTED, project_item, user: user) result end |