Module: PlanMyStuff::Pipeline
- Defined in:
- lib/plan_my_stuff/pipeline.rb,
lib/plan_my_stuff/pipeline/status.rb,
lib/plan_my_stuff/pipeline/issue_linker.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: IssueLinker, Status
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”.
-
.remove!(project_item) ⇒ String?
Removes a project item from the pipeline project entirely.
-
.request_testing!(project_item) ⇒ Hash
Moves a project item to “Testing”.
-
.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”.
-
.submit!(issue, assignee:, project_number: nil) ⇒ PlanMyStuff::ProjectItem
Adds an issue to the pipeline project, moves it to “Submitted”, and assigns the given developer.
-
.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”).
282 283 284 285 286 287 288 289 290 291 |
# File 'lib/plan_my_stuff/pipeline.rb', line 282 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 (submit!, 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.
36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/plan_my_stuff/pipeline.rb', line 36 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.
68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/plan_my_stuff/pipeline.rb', line 68 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.
164 165 166 167 168 169 170 171 172 173 |
# File 'lib/plan_my_stuff/pipeline.rb', line 164 def late_removal?(prior_status) return false if prior_status.nil? early_statuses = [ resolve_status_name(PlanMyStuff::Pipeline::Status::SUBMITTED), resolve_status_name(PlanMyStuff::Pipeline::Status::STARTED), ] early_statuses.exclude?(prior_status) end |
.mark_in_review!(project_item) ⇒ Hash
Moves a project item to “In Review”.
196 197 198 199 200 201 202 203 |
# File 'lib/plan_my_stuff/pipeline.rb', line 196 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”.
226 227 228 229 230 231 232 233 |
# File 'lib/plan_my_stuff/pipeline.rb', line 226 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 |
.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 “Submitted” or “Started”, accounting for configured status aliases). nil status is treated as not-late.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/plan_my_stuff/pipeline.rb', line 137 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
Moves a project item to “Testing”.
211 212 213 214 215 216 217 218 |
# File 'lib/plan_my_stuff/pipeline.rb', line 211 def request_testing!(project_item) guard_approvals!(project_item&.issue) status = resolve_status_name(PlanMyStuff::Pipeline::Status::TESTING) result = project_item.move_to!(status) instrument(PlanMyStuff::Pipeline::Status::TESTING, project_item) 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.
89 90 91 92 93 94 95 96 |
# File 'lib/plan_my_stuff/pipeline.rb', line 89 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.
56 57 58 |
# File 'lib/plan_my_stuff/pipeline.rb', line 56 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.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/plan_my_stuff/pipeline.rb', line 250 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 |
.submit!(issue, assignee:, project_number: nil) ⇒ PlanMyStuff::ProjectItem
Adds an issue to the pipeline project, moves it to “Submitted”, and assigns the given developer.
Called when an issue is assigned to a dev, NOT on issue creation. Un-triaged issues should not be in the pipeline.
110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/plan_my_stuff/pipeline.rb', line 110 def submit!(issue, assignee:, project_number: nil) guard_approvals!(issue) number = resolve_pipeline_project_number(project_number) project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number) status = resolve_status_name(PlanMyStuff::Pipeline::Status::SUBMITTED) project_item.move_to!(status) project_item.assign!(assignee) instrument(PlanMyStuff::Pipeline::Status::SUBMITTED, project_item) project_item end |
.take!(project_item) ⇒ Hash
Moves a project item to “Started”.
181 182 183 184 185 186 187 188 |
# File 'lib/plan_my_stuff/pipeline.rb', line 181 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 |