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
-
.complete_deployment!(project_item, deployment_id: nil) ⇒ Hash?
Moves a project item to “Completed” if the linked issue has
auto_completeenabled. -
.guard_approvals!(issue) ⇒ void
Raises
PlanMyStuff::PendingApprovalsErrorwhenissuehas pending manager approvals. -
.instrument(status, project_item, **extra) ⇒ void
Fires an
ActiveSupport::Notificationsevent for a pipeline transition. -
.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) ⇒ 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) ⇒ Hash
Moves a project item to “Started”.
Class Method Details
.complete_deployment!(project_item, deployment_id: nil) ⇒ Hash?
Moves a project item to “Completed” if the linked issue has auto_complete enabled. Returns nil when auto-complete is off (item stays at “Release in Progress”).
287 288 289 290 291 292 293 294 295 296 |
# File 'lib/plan_my_stuff/pipeline.rb', line 287 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) 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.
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/plan_my_stuff/pipeline.rb', line 38 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(status, project_item, **extra) ⇒ void
This method returns an undefined value.
Fires an ActiveSupport::Notifications event for a pipeline transition.
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/plan_my_stuff/pipeline.rb', line 70 def instrument(status, project_item, **extra) key = PlanMyStuff::Pipeline::Status.key_for(status) payload = { project_item: project_item, issue_number: project_item.number, status: status, timestamp: Time.current, }.merge(extra) ActiveSupport::Notifications.instrument("plan_my_stuff.pipeline.#{key}", payload) 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.
140 141 142 143 144 145 |
# File 'lib/plan_my_stuff/pipeline.rb', line 140 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”.
190 191 192 193 194 195 196 197 |
# File 'lib/plan_my_stuff/pipeline.rb', line 190 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”.
231 232 233 234 235 236 237 238 |
# File 'lib/plan_my_stuff/pipeline.rb', line 231 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”.
159 160 161 162 163 164 165 166 167 |
# File 'lib/plan_my_stuff/pipeline.rb', line 159 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 plan_my_stuff.pipeline.removed. Additionally fires plan_my_stuff.pipeline.removed_late when prior_status is past “Started” (i.e. anything other than “Started”, accounting for configured status aliases). nil status is treated as not-late.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/plan_my_stuff/pipeline.rb', line 113 def remove!(project_item) prior_status = project_item.status result = project_item.destroy! payload = { project_item: project_item, issue_number: project_item.number, prior_status: prior_status, timestamp: Time.current, } ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed', payload) if late_removal?(prior_status) ActiveSupport::Notifications.instrument('plan_my_stuff.pipeline.removed_late', payload) end result end |
.request_testing!(project_item) ⇒ 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.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/plan_my_stuff/pipeline.rb', line 207 def request_testing!(project_item) 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) ActiveSupport::Notifications.instrument( 'plan_my_stuff.pipeline.testing', project_item: project_item, issue_number: project_item.number, field_name: field_name, value: value, timestamp: Time.current, ) 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.
91 92 93 94 95 96 97 98 |
# File 'lib/plan_my_stuff/pipeline.rb', line 91 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.
58 59 60 |
# File 'lib/plan_my_stuff/pipeline.rb', line 58 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.
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/plan_my_stuff/pipeline.rb', line 255 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) ⇒ Hash
Moves a project item to “Started”.
175 176 177 178 179 180 181 182 |
# File 'lib/plan_my_stuff/pipeline.rb', line 175 def take!(project_item) guard_approvals!(project_item&.issue) status = resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED) result = project_item.move_to!(status) instrument(PlanMyStuff::Pipeline::Status::STARTED, project_item) result end |