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

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”).

Parameters:

Returns:

  • (Hash, nil)

    mutation result or nil



265
266
267
268
269
270
271
272
273
274
# File 'lib/plan_my_stuff/pipeline.rb', line 265

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.

Parameters:

Raises:



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/plan_my_stuff/pipeline.rb', line 33

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, project_item, **extra) ⇒ void

This method returns an undefined value.

Fires a plan_my_stuff.pipeline.<event> 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”).

Parameters:

  • event (String)

    status name or literal event suffix

  • project_item (PlanMyStuff::BaseProjectItem)
  • extra (Hash)

    additional payload entries



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/plan_my_stuff/pipeline.rb', line 69

def instrument(event, project_item, **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

  PlanMyStuff::Notifications.instrument(
    "pipeline.#{event_to_use}",
    project_item,
    issue_number: project_item.number,
    **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.

Parameters:

  • prior_status (String, nil)

Returns:

  • (Boolean)


131
132
133
134
135
136
# File 'lib/plan_my_stuff/pipeline.rb', line 131

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”.

Parameters:

Returns:

  • (Hash)

    mutation result



179
180
181
182
183
184
185
186
# File 'lib/plan_my_stuff/pipeline.rb', line 179

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”.

Parameters:

Returns:

  • (Hash)

    mutation result



212
213
214
215
216
217
218
219
# File 'lib/plan_my_stuff/pipeline.rb', line 212

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”.

Parameters:

Returns:

  • (Boolean)


148
149
150
151
152
153
154
155
156
# File 'lib/plan_my_stuff/pipeline.rb', line 148

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.

Parameters:

Returns:

  • (String, nil)

    the deleted item ID, or nil if already destroyed



114
115
116
117
118
119
120
121
122
# File 'lib/plan_my_stuff/pipeline.rb', line 114

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) ⇒ 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.

Parameters:

Returns:

  • (Hash)

    mutation result



195
196
197
198
199
200
201
202
203
204
# File 'lib/plan_my_stuff/pipeline.rb', line 195

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)

  instrument('testing', project_item, field_name: field_name, value: value)

  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.

Parameters:

  • project_number (Integer, nil) (defaults to: nil)

Returns:

  • (Integer)

Raises:

  • (ArgumentError)

    if no project number can be resolved



94
95
96
97
98
99
100
101
# File 'lib/plan_my_stuff/pipeline.rb', line 94

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.

Parameters:

  • canonical (String)

    one of the Pipeline::Status constants

Returns:

  • (String)


53
54
55
# File 'lib/plan_my_stuff/pipeline.rb', line 53

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.

Parameters:

  • project_number (Integer, nil) (defaults to: nil)
  • commit_sha (String, nil) (defaults to: nil)

Returns:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/plan_my_stuff/pipeline.rb', line 234

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”.

Parameters:

Returns:

  • (Hash)

    mutation result



164
165
166
167
168
169
170
171
# File 'lib/plan_my_stuff/pipeline.rb', line 164

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