Class: Shipit::Stack

Inherits:
Record
  • Object
show all
Includes:
DeferredTouch
Defined in:
app/models/shipit/stack.rb

Direct Known Subclasses

ReviewStack

Defined Under Namespace

Modules: NoDeployedCommit

Constant Summary collapse

ENVIRONMENT_MAX_SIZE =
50
REQUIRED_HOOKS =
%i[push status].freeze

Constants included from DeferredTouch

DeferredTouch::CACHE_KEY, DeferredTouch::SET_KEY, DeferredTouch::THROTTLE_TTL, DeferredTouch::TMP_KEY

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DeferredTouch

touch_now!

Methods inherited from Record

serializer_class

Class Method Details

.from_param!(param) ⇒ Object



515
516
517
518
519
520
521
522
523
524
525
# File 'app/models/shipit/stack.rb', line 515

def self.from_param!(param)
  repo_owner, repo_name, environment = param.split('/')
  includes(:repository)
    .where(
      repositories: {
        owner: repo_owner.downcase,
        name: repo_name.downcase
      },
      environment:
    ).first!
end

.refresh_deployed_revisionsObject



119
120
121
122
123
124
125
126
127
# File 'app/models/shipit/stack.rb', line 119

def self.refresh_deployed_revisions
  # where.not avoids deserializing every stack's cached_deploy_spec each
  # minute: a stack without a cached spec cannot have fetch steps.
  not_archived
    .where.not(cached_deploy_spec: nil)
    .find_each
    .select(&:supports_fetch_deployed_revision?)
    .each(&:async_refresh_deployed_revision)
end

.run_deploy_in_foreground(stack:, revision:) ⇒ Object



506
507
508
509
510
511
512
513
# File 'app/models/shipit/stack.rb', line 506

def self.run_deploy_in_foreground(stack:, revision:)
  stack = Shipit::Stack.from_param!(stack)
  until_commit = stack.commits.where(sha: revision).limit(1).first
  env = stack.cached_deploy_spec.default_deploy_env
  current_user = Shipit::CommandLineUser.new

  stack.trigger_deploy(until_commit, current_user, env:, force: true, run_now: true)
end

.schedule_continuous_deliveryObject



129
130
131
132
133
# File 'app/models/shipit/stack.rb', line 129

def self.schedule_continuous_delivery
  not_archived.where(continuous_deployment: true).find_each do |stack|
    ContinuousDeliveryJob.perform_later(stack)
  end
end

Instance Method Details

#acquire_git_cache_lock(timeout: 15, &block) ⇒ Object



407
408
409
410
# File 'app/models/shipit/stack.rb', line 407

def acquire_git_cache_lock(timeout: 15, &block)
  @git_cache_lock ||= Flock.new("#{git_path}.lock")
  @git_cache_lock.lock(timeout:, &block)
end

#active_taskObject



463
464
465
466
467
# File 'app/models/shipit/stack.rb', line 463

def active_task
  return @active_task if defined?(@active_task)

  @active_task ||= tasks.current
end

#active_task?Boolean

Returns:

  • (Boolean)


459
460
461
# File 'app/models/shipit/stack.rb', line 459

def active_task?
  !!active_task
end

#allows_merges?Boolean

Returns:

  • (Boolean)


380
381
382
# File 'app/models/shipit/stack.rb', line 380

def allows_merges?
  merge_queue_enabled? && !locked? && merge_status == 'success'
end

#archive!(user) ⇒ Object



494
495
496
# File 'app/models/shipit/stack.rb', line 494

def archive!(user)
  update!(archived_since: Time.now, lock_reason: "Archived", lock_author: user)
end

#archived?Boolean

Returns:

  • (Boolean)


490
491
492
# File 'app/models/shipit/stack.rb', line 490

def archived?
  archived_since.present?
end

#async_refresh_deployed_revisionObject



252
253
254
255
256
# File 'app/models/shipit/stack.rb', line 252

def async_refresh_deployed_revision
  async_refresh_deployed_revision!
rescue StandardError => e
  logger.warn("Failed to dispatch FetchDeployedRevisionJob: [#{e.class.name}] #{e.message}")
end

#async_refresh_deployed_revision!Object



258
259
260
# File 'app/models/shipit/stack.rb', line 258

def async_refresh_deployed_revision!
  FetchDeployedRevisionJob.perform_later(self)
end

#async_update_estimated_deploy_durationObject



600
601
602
# File 'app/models/shipit/stack.rb', line 600

def async_update_estimated_deploy_duration
  UpdateEstimatedDeployDurationJob.perform_later(self)
end

#backlogged?(backlog_leniency_factor: 2.0) ⇒ Boolean

Returns:

  • (Boolean)


294
295
296
# File 'app/models/shipit/stack.rb', line 294

def backlogged?(backlog_leniency_factor: 2.0)
  maximum_commits_per_deploy && (undeployed_commits_count > maximum_commits_per_deploy * backlog_leniency_factor)
end

#base_pathObject



395
396
397
# File 'app/models/shipit/stack.rb', line 395

def base_path
  @base_path ||= Rails.root.join('data', 'stacks', repo_owner, repo_name, environment)
end

#branch_statusObject



298
299
300
301
302
303
304
# File 'app/models/shipit/stack.rb', line 298

def branch_status
  undeployed_commits.each do |commit|
    state = commit.status.simple_state
    return state unless %w[pending unknown missing].freeze.include?(state)
  end
  'pending'
end

#broadcast_updateObject



561
562
563
564
565
566
567
# File 'app/models/shipit/stack.rb', line 561

def broadcast_update
  Pubsubstub.publish(
    "stack.#{id}",
    { id:, updated_at: }.to_json,
    name: 'update'
  )
end

#build_deploy(until_commit, user, env: nil, force: false, allow_concurrency: force) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/models/shipit/stack.rb', line 161

def build_deploy(until_commit, user, env: nil, force: false, allow_concurrency: force)
  since_commit = last_deployed_commit.presence || commits.first
  deploys.build(
    user_id: user.id,
    until_commit:,
    since_commit:,
    env: filter_deploy_envs(env.to_h),
    allow_concurrency:,
    ignored_safeties: force || !until_commit.deployable?,
    max_retries: retries_on_deploy
  )
end

#checklistObject



541
542
543
# File 'app/models/shipit/stack.rb', line 541

def checklist
  cached_deploy_spec.review_checklist
end

#checks?Boolean

Returns:

  • (Boolean)


545
546
547
# File 'app/models/shipit/stack.rb', line 545

def checks?
  cached_deploy_spec.review_checks.present?
end

#ci_enabled?Boolean

Returns:

  • (Boolean)


573
574
575
576
577
# File 'app/models/shipit/stack.rb', line 573

def ci_enabled?
  Rails.cache.fetch(ci_enabled_cache_key) do
    commits.joins(:statuses).any? || commits.joins(:check_runs).any?
  end
end

#clear_git_cache!Object



412
413
414
415
416
417
418
419
420
# File 'app/models/shipit/stack.rb', line 412

def clear_git_cache!
  tmp_path = "#{git_path}-#{SecureRandom.hex}"
  return unless git_path.exist?

  acquire_git_cache_lock do
    git_path.rename(tmp_path)
  end
  FileUtils.rm_rf(tmp_path)
end

#clear_local_filesObject



623
624
625
# File 'app/models/shipit/stack.rb', line 623

def clear_local_files
  FileUtils.rm_rf(base_path.to_s)
end

#continuous_delivery_delayed!Object



206
207
208
# File 'app/models/shipit/stack.rb', line 206

def continuous_delivery_delayed!
  touch(:continuous_delivery_delayed_since) unless continuous_delivery_delayed?
end

#continuous_delivery_delayed?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'app/models/shipit/stack.rb', line 202

def continuous_delivery_delayed?
  continuous_delivery_delayed_since? && continuous_deployment? && (checks? || deployment_checks?)
end

#continuous_delivery_resumed!Object



198
199
200
# File 'app/models/shipit/stack.rb', line 198

def continuous_delivery_resumed!
  update!(continuous_delivery_delayed_since: nil)
end

#deployable?Boolean

Returns:

  • (Boolean)


376
377
378
# File 'app/models/shipit/stack.rb', line 376

def deployable?
  !locked? && !active_task? && !awaiting_provision? && deployment_checks_passed?
end

#deployed_too_recently?Boolean

Returns:

  • (Boolean)


245
246
247
248
249
250
# File 'app/models/shipit/stack.rb', line 245

def deployed_too_recently?
  return unless task = last_active_task
  return true if task.validating?

  task.ended_at? && (task.ended_at + pause_between_deploys).future?
end

#deployment_checks_passed?Boolean

Returns:

  • (Boolean)


627
628
629
630
631
# File 'app/models/shipit/stack.rb', line 627

def deployment_checks_passed?
  return true unless deployment_checks?

  Shipit.deployment_checks.call(self)
end

#deploys_pathObject



399
400
401
# File 'app/models/shipit/stack.rb', line 399

def deploys_path
  @deploys_path ||= base_path.join("deploys")
end

#emit_lock_hooksObject



633
634
635
636
637
638
639
640
641
# File 'app/models/shipit/stack.rb', line 633

def emit_lock_hooks
  return unless previous_changes.include?('lock_reason')

  lock_details = if previous_changes['lock_reason'].last.blank?
                   { from: previous_changes['locked_since'].first, until: Time.zone.now }
                 end

  Hook.emit(:lock, self, locked: locked?, lock_details:, stack: self)
end

#enable_ci!Object



579
580
581
# File 'app/models/shipit/stack.rb', line 579

def enable_ci!
  Rails.cache.write(ci_enabled_cache_key, true)
end

#envObject



54
55
56
57
58
59
60
61
62
63
# File 'app/models/shipit/stack.rb', line 54

def env
  {
    'ENVIRONMENT' => environment,
    'LAST_DEPLOYED_SHA' => last_deployed_commit.sha,
    'GITHUB_REPO_OWNER' => repository.owner,
    'GITHUB_REPO_NAME' => repository.name,
    'DEPLOY_URL' => deploy_url,
    'BRANCH' => branch
  }
end

#git_pathObject



403
404
405
# File 'app/models/shipit/stack.rb', line 403

def git_path
  @git_path ||= base_path.join("git")
end

#github_apiObject



434
435
436
# File 'app/models/shipit/stack.rb', line 434

def github_api
  github_app.api
end

#github_appObject



438
439
440
# File 'app/models/shipit/stack.rb', line 438

def github_app
  Shipit.github(organization: repository.owner)
end

#github_commitsObject



426
427
428
429
430
431
432
# File 'app/models/shipit/stack.rb', line 426

def github_commits
  handle_github_redirections do
    github_api.commits(github_repo_name, sha: branch)
  end
rescue Octokit::Conflict
  [] # Repository is empty...
end

#github_repo_nameObject



422
423
424
# File 'app/models/shipit/stack.rb', line 422

def github_repo_name
  repository.github_repo_name
end

#handle_github_redirectionsObject



442
443
444
445
446
447
448
449
450
451
# File 'app/models/shipit/stack.rb', line 442

def handle_github_redirections
  # https://developer.github.com/v3/#http-redirects
  resource = yield
  if resource.try(:message) == 'Moved Permanently'
    refresh_repository!
    yield
  else
    resource
  end
end

#headObject



282
283
284
# File 'app/models/shipit/stack.rb', line 282

def head
  commits.reachable.first&.sha
end

#inaccessible?Boolean

Returns:

  • (Boolean)


591
592
593
# File 'app/models/shipit/stack.rb', line 591

def inaccessible?
  inaccessible_since?
end

#last_active_taskObject



364
365
366
# File 'app/models/shipit/stack.rb', line 364

def last_active_task
  tasks.exclusive.last
end

#last_completed_deployObject



352
353
354
# File 'app/models/shipit/stack.rb', line 352

def last_completed_deploy
  deploys_and_rollbacks.last_completed
end

#last_deployed_commitObject



368
369
370
# File 'app/models/shipit/stack.rb', line 368

def last_deployed_commit
  last_completed_deploy&.until_commit || NoDeployedCommit
end

#last_successful_deploy_commitObject



356
357
358
# File 'app/models/shipit/stack.rb', line 356

def last_successful_deploy_commit
  deploys_and_rollbacks.last_successful&.until_commit
end


616
617
618
619
620
621
# File 'app/models/shipit/stack.rb', line 616

def links
  links_spec = cached_deploy_spec&.links || {}
  context = EnvironmentVariables.with(env)

  links_spec.transform_values { |url| context.interpolate(url) }
end

#lock(reason, user) ⇒ Object



481
482
483
484
# File 'app/models/shipit/stack.rb', line 481

def lock(reason, user)
  params = { lock_reason: reason, lock_author: user }
  update!(params)
end

#lock_authorObject



69
70
71
# File 'app/models/shipit/stack.rb', line 69

def lock_author(*)
  super || AnonymousUser.new
end

#lock_author=(user) ⇒ Object



73
74
75
# File 'app/models/shipit/stack.rb', line 73

def lock_author=(user)
  super(user&.logged_in? ? user : nil)
end

#lock_reverted_commits!Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/shipit/stack.rb', line 312

def lock_reverted_commits!
  backlog = undeployed_commits.to_a
  affected_rows = 0

  until backlog.empty?
    backlog = backlog.drop_while { |c| !c.revert? }
    revert = backlog.shift
    next if revert.nil?

    commits_to_lock = backlog.reverse.drop_while { |c| !revert.revert_of?(c) }
    next if commits_to_lock.empty?

    affected_rows += commits
                     .where(id: commits_to_lock.map(&:id).uniq)
                     .lock_all(revert.author)
  end

  touch if affected_rows > 1
end

#locked?Boolean

Returns:

  • (Boolean)


477
478
479
# File 'app/models/shipit/stack.rb', line 477

def locked?
  lock_reason.present?
end

#mark_as_accessible!Object



583
584
585
# File 'app/models/shipit/stack.rb', line 583

def mark_as_accessible!
  update!(inaccessible_since: nil)
end

#mark_as_inaccessible!Object



587
588
589
# File 'app/models/shipit/stack.rb', line 587

def mark_as_inaccessible!
  update!(inaccessible_since: Time.now) unless inaccessible?
end

#merge_methodObject



384
385
386
# File 'app/models/shipit/stack.rb', line 384

def merge_method
  cached_deploy_spec&.merge_request_merge_method || Shipit.default_merge_method
end

#merge_status(backlog_leniency_factor: 2.0) ⇒ Object



286
287
288
289
290
291
292
# File 'app/models/shipit/stack.rb', line 286

def merge_status(backlog_leniency_factor: 2.0)
  return 'locked' if locked?
  return 'failure' if %w[failure error].freeze.include?(branch_status)
  return 'backlogged' if backlogged?(backlog_leniency_factor:)

  'success'
end

#monitoringObject



537
538
539
# File 'app/models/shipit/stack.rb', line 537

def monitoring
  cached_deploy_spec.review_monitoring
end

#monitoring?Boolean

Returns:

  • (Boolean)


533
534
535
# File 'app/models/shipit/stack.rb', line 533

def monitoring?
  monitoring.present?
end

#next_commit_to_deployObject



235
236
237
238
239
240
241
242
243
# File 'app/models/shipit/stack.rb', line 235

def next_commit_to_deploy
  commits_to_deploy = commits.order(id: :asc).newer_than(last_deployed_commit).reachable.preload(:statuses)
  if maximum_commits_per_deploy
    commits_with_max_applied = commits_to_deploy.limit(maximum_commits_per_deploy)
    deployable_commits(commits_with_max_applied) || deployable_commits(commits_to_deploy)
  else
    deployable_commits(commits_to_deploy)
  end
end

#next_expected_commit_to_deploy(commits: nil) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
# File 'app/models/shipit/stack.rb', line 332

def next_expected_commit_to_deploy(commits: nil)
  commits ||= undeployed_commits do |scope|
    scope.preload(:statuses, :check_runs)
  end

  commits_to_deploy = commits.reject(&:active?)
  if maximum_commits_per_deploy
    commits_to_deploy = commits_to_deploy.reverse.slice(0, maximum_commits_per_deploy).reverse
  end
  commits_to_deploy.find(&:deployable?)
end

#occupiedObject



473
474
475
# File 'app/models/shipit/stack.rb', line 473

def occupied
  @occupied ||= tasks.active.last
end

#occupied?Boolean

Returns:

  • (Boolean)


469
470
471
# File 'app/models/shipit/stack.rb', line 469

def occupied?
  !!occupied
end

#previous_successful_deploy(deploy_id) ⇒ Object



360
361
362
# File 'app/models/shipit/stack.rb', line 360

def previous_successful_deploy(deploy_id)
  deploys_and_rollbacks.success.where("id < ?", deploy_id).last
end

#previous_successful_deploy_commit(deploy_id) ⇒ Object



372
373
374
# File 'app/models/shipit/stack.rb', line 372

def previous_successful_deploy_commit(deploy_id)
  previous_successful_deploy(deploy_id)&.until_commit || NoDeployedCommit
end

#recent_deploys_durationsObject



608
609
610
# File 'app/models/shipit/stack.rb', line 608

def recent_deploys_durations
  tasks.where(type: 'Shipit::Deploy').success.order(id: :desc).limit(100).durations
end

#refresh_repository!Object



453
454
455
456
457
# File 'app/models/shipit/stack.rb', line 453

def refresh_repository!
  resource = github_api.repo(github_repo_name)
  resource = github_api.get(resource.url) if resource.try(:message) == 'Moved Permanently'
  repository.update!(owner: resource.owner., name: resource.name)
end

#reloadObject



595
596
597
598
# File 'app/models/shipit/stack.rb', line 595

def reload(*)
  clear_cache
  super
end

#repositoryObject



65
66
67
# File 'app/models/shipit/stack.rb', line 65

def repository
  super || build_repository
end

#schedule_for_destroy!Object



569
570
571
# File 'app/models/shipit/stack.rb', line 569

def schedule_for_destroy!
  DestroyStackJob.perform_later(self)
end

#schedule_mergesObject



231
232
233
# File 'app/models/shipit/stack.rb', line 231

def schedule_merges
  ProcessMergeRequestsJob.perform_later(self)
end

#statusObject



306
307
308
309
310
# File 'app/models/shipit/stack.rb', line 306

def status
  return :deploying if active_task?

  :default
end

#sync_github(expected_head_sha: nil) ⇒ Object



612
613
614
# File 'app/models/shipit/stack.rb', line 612

def sync_github(expected_head_sha: nil)
  GithubSyncJob.perform_later(stack_id: id, expected_head_sha:)
end

#sync_github_if_necessaryObject



90
91
92
93
94
# File 'app/models/shipit/stack.rb', line 90

def sync_github_if_necessary
  return unless (archived_since_previously_changed? && archived_since.nil?) || branch_previously_changed?

  sync_github
end

#to_paramObject



502
503
504
# File 'app/models/shipit/stack.rb', line 502

def to_param
  [repo_owner, repo_name, environment].join('/')
end

#trigger_continuous_deliveryObject



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/shipit/stack.rb', line 210

def trigger_continuous_delivery
  return if cached_deploy_spec.blank?

  commit = next_commit_to_deploy

  if should_resume_continuous_delivery?(commit)
    continuous_delivery_resumed!
    return
  end

  if should_delay_continuous_delivery?(commit)
    continuous_delivery_delayed!
    return
  end

  begin
    trigger_deploy(commit, Shipit.user, env: cached_deploy_spec.default_deploy_env)
  rescue Task::ConcurrentTaskRunning
  end
end

#trigger_deploy(*args, **kwargs) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/shipit/stack.rb', line 174

def trigger_deploy(*args, **kwargs)
  if changed?
    # If this is the first deploy since the spec changed it's possible the record will be dirty here, meaning we
    # cant lock. In this one case persist the changes, otherwise log a warning and let the lock raise, so we
    # can debug what's going on here. We don't expect anything other than the deploy spec to dirty the model
    # instance, because of how that field is serialised.
    if changes.keys == ['cached_deploy_spec']
      save!
    else
      Rails.logger.warning("#{changes.keys} field(s) were unexpectedly modified on stack #{id} while deploying")
    end
  end

  run_now = kwargs.delete(:run_now)
  deploy = with_lock do
    deploy = build_deploy(*args, **kwargs)
    deploy.save!
    deploy
  end
  run_now ? deploy.run_now! : deploy.enqueue
  continuous_delivery_resumed!
  deploy
end

#trigger_task(definition_id, user, env: nil, force: false) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/models/shipit/stack.rb', line 139

def trigger_task(definition_id, user, env: nil, force: false)
  definition = find_task_definition(definition_id)
  env = env.to_h

  definition.variables_with_defaults.each do |variable|
    env[variable.name] ||= variable.default
  end

  commit = last_deployed_commit.presence || commits.first
  task = tasks.create(
    user_id: user.id,
    definition:,
    until_commit_id: commit.id,
    since_commit_id: commit.id,
    env: definition.filter_envs(env),
    allow_concurrency: definition.allow_concurrency? || force,
    ignored_safeties: force
  )
  task.enqueue
  task
end

#unarchive!Object



498
499
500
# File 'app/models/shipit/stack.rb', line 498

def unarchive!
  update!(archived_since: nil, lock_reason: nil, lock_author: nil, locked_since: nil)
end

#undeployed_commitsObject



344
345
346
347
348
349
350
# File 'app/models/shipit/stack.rb', line 344

def undeployed_commits
  scope = commits.reachable.newer_than(last_deployed_commit).order(id: :asc)

  scope = yield scope if block_given?

  scope.to_a.reverse
end

#undeployed_commits?Boolean

Returns:

  • (Boolean)


135
136
137
# File 'app/models/shipit/stack.rb', line 135

def undeployed_commits?
  undeployed_commits_count > 0
end

#unlockObject



486
487
488
# File 'app/models/shipit/stack.rb', line 486

def unlock
  update!(lock_reason: nil, lock_author: nil, locked_since: nil)
end

#update_deployed_revision(sha) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'app/models/shipit/stack.rb', line 262

def update_deployed_revision(sha)
  last_deploy = deploys_and_rollbacks.last
  return if last_deploy&.active?

  actual_deployed_commit = commits.reachable.by_sha(sha)
  return unless actual_deployed_commit

  if last_deploy && actual_deployed_commit == last_deploy.until_commit
    last_deploy.accept!
  elsif last_deploy && actual_deployed_commit == last_deploy.since_commit
    last_deploy.reject!
  else
    deploys.create!(
      until_commit: actual_deployed_commit,
      since_commit: last_deployed_commit.presence || commits.first,
      status: 'success'
    )
  end
end

#update_estimated_deploy_duration!Object



604
605
606
# File 'app/models/shipit/stack.rb', line 604

def update_estimated_deploy_duration!
  update!(estimated_deploy_duration: Stat.p90(recent_deploys_durations) || 1)
end

#update_latest_deployed_refObject



555
556
557
558
559
# File 'app/models/shipit/stack.rb', line 555

def update_latest_deployed_ref
  return unless Shipit.update_latest_deployed_ref

  UpdateGithubLastDeployedRefJob.perform_later(self)
end

#update_undeployed_commits_count(after_commit = nil) ⇒ Object



549
550
551
552
553
# File 'app/models/shipit/stack.rb', line 549

def update_undeployed_commits_count(after_commit = nil)
  after_commit ||= last_deployed_commit
  undeployed_commits = commits.reachable.newer_than(after_commit).count
  update(undeployed_commits_count: undeployed_commits)
end