shipit-engine 0.27.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -1
  3. data/app/assets/stylesheets/_pages/_commits.scss +2 -0
  4. data/app/assets/stylesheets/_pages/_deploy.scss +6 -0
  5. data/app/controllers/shipit/api/release_statuses_controller.rb +22 -0
  6. data/app/controllers/shipit/api/stacks_controller.rb +6 -1
  7. data/app/controllers/shipit/commits_controller.rb +12 -1
  8. data/app/controllers/shipit/deploys_controller.rb +11 -0
  9. data/app/controllers/shipit/stacks_controller.rb +29 -1
  10. data/app/controllers/shipit/tasks_controller.rb +13 -1
  11. data/app/helpers/shipit/merge_status_helper.rb +2 -2
  12. data/app/helpers/shipit/stacks_helper.rb +10 -4
  13. data/app/jobs/shipit/perform_task_job.rb +1 -0
  14. data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +41 -0
  15. data/app/models/shipit/command_line_user.rb +58 -0
  16. data/app/models/shipit/commit.rb +42 -2
  17. data/app/models/shipit/deploy.rb +31 -2
  18. data/app/models/shipit/deploy_spec/lerna_discovery.rb +43 -19
  19. data/app/models/shipit/deploy_spec/pypi_discovery.rb +5 -1
  20. data/app/models/shipit/rollback.rb +4 -2
  21. data/app/models/shipit/stack.rb +72 -15
  22. data/app/models/shipit/task.rb +30 -0
  23. data/app/models/shipit/undeployed_commit.rb +10 -1
  24. data/app/serializers/shipit/command_line_user_serializer.rb +4 -0
  25. data/app/views/layouts/shipit.html.erb +5 -1
  26. data/app/views/shipit/commits/_commit.html.erb +7 -2
  27. data/app/views/shipit/merge_status/_commit_count_warning.html.erb +1 -5
  28. data/app/views/shipit/merge_status/backlogged.html.erb +1 -1
  29. data/app/views/shipit/merge_status/failure.html.erb +1 -1
  30. data/app/views/shipit/merge_status/locked.html.erb +1 -1
  31. data/app/views/shipit/merge_status/success.html.erb +1 -1
  32. data/app/views/shipit/stacks/show.html.erb +10 -1
  33. data/app/views/shipit/tasks/_task_output.html.erb +2 -2
  34. data/config/locales/en.yml +2 -1
  35. data/config/routes.rb +8 -1
  36. data/db/migrate/20190502020249_add_lock_author_id_to_commits.rb +5 -0
  37. data/lib/shipit.rb +14 -2
  38. data/lib/shipit/cast_value.rb +9 -0
  39. data/lib/shipit/command.rb +62 -16
  40. data/lib/shipit/line_buffer.rb +42 -0
  41. data/lib/shipit/version.rb +1 -1
  42. data/lib/tasks/shipit.rake +27 -0
  43. data/test/controllers/api/release_statuses_controller_test.rb +66 -0
  44. data/test/controllers/api/stacks_controller_test.rb +19 -0
  45. data/test/controllers/commits_controller_test.rb +30 -6
  46. data/test/controllers/deploys_controller_test.rb +51 -2
  47. data/test/controllers/tasks_controller_test.rb +24 -0
  48. data/test/dummy/db/schema.rb +2 -1
  49. data/test/dummy/db/seeds.rb +2 -0
  50. data/test/fixtures/shipit/check_runs.yml +11 -0
  51. data/test/fixtures/shipit/commits.yml +104 -0
  52. data/test/fixtures/shipit/stacks.yml +98 -3
  53. data/test/fixtures/shipit/tasks.yml +42 -0
  54. data/test/jobs/update_github_last_deployed_ref_job_test.rb +88 -0
  55. data/test/models/commits_test.rb +88 -1
  56. data/test/models/deploy_spec_test.rb +34 -6
  57. data/test/models/deploys_test.rb +308 -6
  58. data/test/models/rollbacks_test.rb +17 -11
  59. data/test/models/stacks_test.rb +217 -4
  60. data/test/models/tasks_test.rb +13 -0
  61. data/test/models/undeployed_commits_test.rb +62 -3
  62. data/test/test_helper.rb +0 -1
  63. data/test/unit/command_test.rb +55 -0
  64. data/test/unit/line_buffer_test.rb +20 -0
  65. metadata +142 -128
@@ -36,9 +36,30 @@ module Shipit
36
36
  after_create :create_commit_deployments
37
37
  after_create :update_release_status
38
38
  after_commit :broadcast_update
39
+ after_commit :update_latest_deployed_ref, on: :update
39
40
 
40
41
  delegate :broadcast_update, :filter_deploy_envs, to: :stack
41
42
 
43
+ def self.newer_than(deploy)
44
+ return all unless deploy
45
+ where('id > ?', deploy.try(:id) || deploy)
46
+ end
47
+
48
+ def self.older_than(deploy)
49
+ return all unless deploy
50
+ where('id < ?', deploy.try(:id) || deploy)
51
+ end
52
+
53
+ def self.since(deploy)
54
+ return all unless deploy
55
+ where('id >= ?', deploy.try(:id) || deploy)
56
+ end
57
+
58
+ def self.until(deploy)
59
+ return all unless deploy
60
+ where('id <= ?', deploy.try(:id) || deploy)
61
+ end
62
+
42
63
  def build_rollback(user = nil, env: nil, force: false)
43
64
  Rollback.new(
44
65
  user_id: user&.id,
@@ -65,16 +86,19 @@ module Shipit
65
86
  rollback
66
87
  end
67
88
 
68
- # Rolls the stack back to the **previous** deploy
89
+ # Rolls the stack back to the most recent **previous** successful deploy
69
90
  def trigger_revert(force: false)
91
+ previous_successful_commit = commit_to_rollback_to
92
+
70
93
  rollback = Rollback.create!(
71
94
  user_id: user_id,
72
95
  stack_id: stack_id,
73
96
  parent_id: id,
74
97
  since_commit: until_commit,
75
- until_commit: since_commit,
98
+ until_commit: previous_successful_commit,
76
99
  allow_concurrency: force,
77
100
  )
101
+
78
102
  rollback.enqueue
79
103
  lock_reason = "A rollback for #{until_commit.sha} has been triggered. " \
80
104
  "Please make sure the reason for the rollback has been addressed before deploying again."
@@ -257,5 +281,10 @@ module Shipit
257
281
  def update_last_deploy_time
258
282
  stack.update(last_deployed_at: ended_at)
259
283
  end
284
+
285
+ def update_latest_deployed_ref
286
+ return unless previous_changes.include?(:status)
287
+ stack.update_latest_deployed_ref if previous_changes[:status].last == 'success'
288
+ end
260
289
  end
261
290
  end
@@ -3,6 +3,8 @@ require 'json'
3
3
  module Shipit
4
4
  class DeploySpec
5
5
  module LernaDiscovery
6
+ LATEST_MAJOR_VERSION = Gem::Version.new('3.0.0')
7
+
6
8
  def discover_dependencies_steps
7
9
  discover_lerna_json || super
8
10
  end
@@ -21,15 +23,21 @@ module Shipit
21
23
 
22
24
  def discover_lerna_checklist
23
25
  if lerna?
26
+ command = if lerna_lerna >= LATEST_MAJOR_VERSION
27
+ 'lerna version'
28
+ else
29
+ %(
30
+ lerna publish --skip-npm
31
+ && git add -A
32
+ && git push --follow-tags
33
+ )
34
+ end
35
+
24
36
  [%(
25
37
  <strong>Don't forget version and tag before publishing!</strong>
26
38
  You can do this with:<br/>
27
- <pre>
28
- lerna publish --skip-npm
29
- && git add -A
30
- && git push --follow-tags
31
- </pre>
32
- )]
39
+ <pre>#{command}</pre>
40
+ )]
33
41
  end
34
42
  end
35
43
 
@@ -41,9 +49,16 @@ module Shipit
41
49
  file('lerna.json')
42
50
  end
43
51
 
52
+ def lerna_config
53
+ @_lerna_config ||= JSON.parse(lerna_json.read)
54
+ end
55
+
56
+ def lerna_lerna
57
+ Gem::Version.new(lerna_config['lerna'])
58
+ end
59
+
44
60
  def lerna_version
45
- lerna_config = lerna_json.read
46
- JSON.parse(lerna_config)['version']
61
+ lerna_config['version']
47
62
  end
48
63
 
49
64
  def discover_lerna_packages
@@ -68,18 +83,27 @@ module Shipit
68
83
 
69
84
  def publish_fixed_version_packages
70
85
  check_tags = 'assert-lerna-fixed-version-tag'
71
- # `yarn publish` requires user input, so always use npm.
72
86
  version = lerna_version
73
- publish = %W(
74
- node_modules/.bin/lerna publish
75
- --yes
76
- --skip-git
77
- --repo-version #{version}
78
- --force-publish=*
79
- --npm-tag #{dist_tag(version)}
80
- --npm-client=npm
81
- --skip-npm=false
82
- ).join(" ")
87
+ publish = if lerna_lerna >= LATEST_MAJOR_VERSION
88
+ %W(
89
+ node_modules/.bin/lerna publish
90
+ from-git
91
+ --yes
92
+ --dist-tag #{dist_tag(version)}
93
+ ).join(" ")
94
+ else
95
+ # `yarn publish` requires user input, so always use npm.
96
+ %W(
97
+ node_modules/.bin/lerna publish
98
+ --yes
99
+ --skip-git
100
+ --repo-version #{version}
101
+ --force-publish=*
102
+ --npm-tag #{dist_tag(version)}
103
+ --npm-client=npm
104
+ --skip-npm=false
105
+ ).join(" ")
106
+ end
83
107
 
84
108
  [check_tags, publish]
85
109
  end
@@ -29,7 +29,11 @@ module Shipit
29
29
  end
30
30
 
31
31
  def publish_egg
32
- ["assert-egg-version-tag #{setup_dot_py}", 'python setup.py register sdist upload']
32
+ [
33
+ "assert-egg-version-tag #{setup_dot_py}",
34
+ 'python setup.py register sdist',
35
+ 'twine upload dist/*',
36
+ ]
33
37
  end
34
38
  end
35
39
  end
@@ -36,9 +36,11 @@ module Shipit
36
36
 
37
37
  def update_release_status
38
38
  return unless stack.release_status?
39
- return unless status == 'pending'
40
39
 
41
- deploy.report_faulty!(description: "A rollback of #{stack.to_param} was triggered")
40
+ # When we rollback to a certain revision, assume that all later deploys were faulty
41
+ stack.deploys.newer_than(deploy.id).until(stack.last_completed_deploy.id).to_a.each do |deploy|
42
+ deploy.report_faulty!(description: "A rollback of #{stack.to_param} was triggered")
43
+ end
42
44
  end
43
45
 
44
46
  def lock_reverted_commits
@@ -13,6 +13,10 @@ module Shipit
13
13
  ''
14
14
  end
15
15
 
16
+ def short_sha
17
+ ''
18
+ end
19
+
16
20
  def blank?
17
21
  true
18
22
  end
@@ -124,10 +128,11 @@ module Shipit
124
128
  )
125
129
  end
126
130
 
127
- def trigger_deploy(*args)
128
- deploy = build_deploy(*args)
131
+ def trigger_deploy(*args, **kwargs)
132
+ run_now = kwargs.delete(:run_now)
133
+ deploy = build_deploy(*args, **kwargs)
129
134
  deploy.save!
130
- deploy.enqueue
135
+ run_now ? deploy.run_now! : deploy.enqueue
131
136
  continuous_delivery_resumed!
132
137
  deploy
133
138
  end
@@ -182,6 +187,12 @@ module Shipit
182
187
  end
183
188
 
184
189
  def async_refresh_deployed_revision
190
+ async_refresh_deployed_revision!
191
+ rescue => error
192
+ logger.warn "Failed to dispatch FetchDeployedRevisionJob: [#{error.class.name}] #{error.message}"
193
+ end
194
+
195
+ def async_refresh_deployed_revision!
185
196
  FetchDeployedRevisionJob.perform_later(self)
186
197
  end
187
198
 
@@ -234,33 +245,57 @@ module Shipit
234
245
  end
235
246
 
236
247
  def lock_reverted_commits!
237
- commits_to_lock = []
238
-
239
248
  backlog = undeployed_commits.to_a
249
+ affected_rows = 0
250
+
240
251
  until backlog.empty?
241
252
  backlog = backlog.drop_while { |c| !c.revert? }
242
- if revert = backlog.shift
243
- commits_to_lock += backlog.reverse.drop_while { |c| !revert.revert_of?(c) }
244
- end
253
+ revert = backlog.shift
254
+ next if revert.nil?
255
+
256
+ commits_to_lock = backlog.reverse.drop_while { |c| !revert.revert_of?(c) }
257
+ next if commits_to_lock.empty?
258
+
259
+ affected_rows += commits
260
+ .where(id: commits_to_lock.map(&:id).uniq)
261
+ .lock_all(revert.author)
245
262
  end
246
263
 
247
- unless commits_to_lock.empty?
248
- if commits.where(id: commits_to_lock.map(&:id).uniq).update_all(locked: true) > 1
249
- touch
250
- end
264
+ touch if affected_rows > 1
265
+ end
266
+
267
+ def next_expected_commit_to_deploy(commits: nil)
268
+ commits ||= undeployed_commits do |scope|
269
+ scope.preload(:statuses, :check_runs)
251
270
  end
271
+
272
+ commits_to_deploy = commits.reject(&:active?)
273
+ if maximum_commits_per_deploy
274
+ commits_to_deploy = commits_to_deploy.reverse.slice(0, maximum_commits_per_deploy).reverse
275
+ end
276
+ commits_to_deploy.find(&:deployable?)
252
277
  end
253
278
 
254
279
  def undeployed_commits
255
280
  scope = commits.reachable.newer_than(last_deployed_commit).order(id: :asc)
256
- yield scope if block_given?
257
- scope.map.with_index { |c, i| UndeployedCommit.new(c, i) }.reverse
281
+
282
+ scope = yield scope if block_given?
283
+
284
+ scope.to_a.reverse
258
285
  end
259
286
 
260
287
  def last_completed_deploy
261
288
  deploys_and_rollbacks.last_completed
262
289
  end
263
290
 
291
+ def last_successful_deploy_commit
292
+ deploys_and_rollbacks.last_successful&.until_commit
293
+ end
294
+
295
+ def previous_successful_deploy(deploy_id)
296
+ deploys_and_rollbacks.success.where("id < ?", deploy_id).last
297
+ end
298
+
264
299
  def last_active_task
265
300
  tasks.exclusive.last
266
301
  end
@@ -269,6 +304,10 @@ module Shipit
269
304
  last_completed_deploy&.until_commit || NoDeployedCommit
270
305
  end
271
306
 
307
+ def previous_successful_deploy_commit(deploy_id)
308
+ previous_successful_deploy(deploy_id)&.until_commit || NoDeployedCommit
309
+ end
310
+
272
311
  def deployable?
273
312
  !locked? && !active_task?
274
313
  end
@@ -379,6 +418,15 @@ module Shipit
379
418
  [repo_owner, repo_name, environment].join('/')
380
419
  end
381
420
 
421
+ def self.run_deploy_in_foreground(stack:, revision:)
422
+ stack = Shipit::Stack.from_param!(stack)
423
+ until_commit = stack.commits.where(sha: revision).limit(1).first
424
+ env = stack.cached_deploy_spec.default_deploy_env
425
+ current_user = Shipit::CommandLineUser.new
426
+
427
+ stack.trigger_deploy(until_commit, current_user, env: env, force: true, run_now: true)
428
+ end
429
+
382
430
  def self.from_param!(param)
383
431
  repo_owner, repo_name, environment = param.split('/')
384
432
  where(
@@ -414,6 +462,10 @@ module Shipit
414
462
  update(undeployed_commits_count: undeployed_commits)
415
463
  end
416
464
 
465
+ def update_latest_deployed_ref
466
+ UpdateGithubLastDeployedRefJob.perform_later(self)
467
+ end
468
+
417
469
  def broadcast_update
418
470
  Pubsubstub.publish(
419
471
  "stack.#{id}",
@@ -502,7 +554,12 @@ module Shipit
502
554
 
503
555
  def emit_lock_hooks
504
556
  return unless previous_changes.include?('lock_reason')
505
- Hook.emit(:lock, self, locked: locked?, stack: self)
557
+
558
+ lock_details = if previous_changes['lock_reason'].last.blank?
559
+ {from: previous_changes['locked_since'].first, until: Time.zone.now}
560
+ end
561
+
562
+ Hook.emit(:lock, self, locked: locked?, lock_details: lock_details, stack: self)
506
563
  end
507
564
 
508
565
  def emit_added_hooks
@@ -47,6 +47,10 @@ module Shipit
47
47
  completed.last
48
48
  end
49
49
 
50
+ def last_successful
51
+ success.last
52
+ end
53
+
50
54
  def current
51
55
  active.exclusive.last
52
56
  end
@@ -176,7 +180,13 @@ module Shipit
176
180
  PerformTaskJob.perform_later(self)
177
181
  end
178
182
 
183
+ def run_now!
184
+ raise "only persisted jobs can be run" unless persisted?
185
+ PerformTaskJob.perform_now(self)
186
+ end
187
+
179
188
  def write(text)
189
+ log_output(text)
180
190
  chunks.create!(text: text)
181
191
  end
182
192
 
@@ -307,6 +317,16 @@ module Shipit
307
317
  Shipit::Engine.routes.url_helpers.stack_task_url(stack, self)
308
318
  end
309
319
 
320
+ def commit_to_rollback_to
321
+ previous_deployed_commit = stack.previous_successful_deploy_commit(id)
322
+
323
+ if previous_deployed_commit == Shipit::Stack::NoDeployedCommit
324
+ since_commit
325
+ else
326
+ previous_deployed_commit
327
+ end
328
+ end
329
+
310
330
  private
311
331
 
312
332
  def prevent_concurrency
@@ -320,5 +340,15 @@ module Shipit
320
340
  def abort_key
321
341
  "#{status_key}:aborting"
322
342
  end
343
+
344
+ def log_output(text)
345
+ output_line_buffer.buffer(text) do |line|
346
+ Shipit.task_logger.info("[#{stack.repo_name}##{id}] #{line}")
347
+ end
348
+ end
349
+
350
+ def output_line_buffer
351
+ @output_line_buffer ||= LineBuffer.new
352
+ end
323
353
  end
324
354
  end
@@ -2,9 +2,10 @@ module Shipit
2
2
  class UndeployedCommit < DelegateClass(Commit)
3
3
  attr_reader :index
4
4
 
5
- def initialize(commit, index)
5
+ def initialize(commit, index:, next_expected_commit_to_deploy: nil)
6
6
  super(commit)
7
7
  @index = index
8
+ @next_expected_commit_to_deploy = next_expected_commit_to_deploy
8
9
  end
9
10
 
10
11
  def deploy_state(bypass_safeties = false)
@@ -38,6 +39,14 @@ module Shipit
38
39
  stack.maximum_commits_per_deploy && index >= stack.maximum_commits_per_deploy
39
40
  end
40
41
 
42
+ def expected_to_be_deployed?
43
+ return false if @next_expected_commit_to_deploy.nil?
44
+ return false unless stack.continuous_deployment
45
+ return false if active?
46
+
47
+ id <= @next_expected_commit_to_deploy.id
48
+ end
49
+
41
50
  def blocked?
42
51
  return @blocked if defined?(@blocked)
43
52
  @blocked = super
@@ -0,0 +1,4 @@
1
+ module Shipit
2
+ class CommandLineUserSerializer < UserSerializer
3
+ end
4
+ end
@@ -1,7 +1,11 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="<%= I18n.locale %>" data-controller="<%= controller_name %>" data-action="<%= action_name %>">
3
3
  <head>
4
- <title><%= [Shipit.app_name, @stack.try!(:repo_name)].compact.join(' - ') %></title>
4
+ <% if @stack %>
5
+ <title><%= Shipit.app_name %> - <%= @stack.repo_name %>/<%= @stack.environment %></title>
6
+ <% else %>
7
+ <title><%= Shipit.app_name %></title>
8
+ <% end %>
5
9
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
10
  <%= favicon_link_tag %>
7
11
  <%= stylesheet_link_tag :shipit, media: 'all' %>
@@ -1,5 +1,5 @@
1
1
  <li class="commit <%= 'locked' if commit.locked? %>" id="commit-<%= commit.id %>">
2
- <% cache commit do %>
2
+ <% cache [commit, commit.expected_to_be_deployed?] do %>
3
3
  <% cache commit.author do %>
4
4
  <%= render 'shipit/shared/author', author: commit.author %>
5
5
  <% end %>
@@ -17,12 +17,17 @@
17
17
  <p class="commit-meta">
18
18
  <%= timeago_tag(commit.committed_at, force: true) %>
19
19
  </p>
20
+ <% if commit.expected_to_be_deployed? %>
21
+ <p class="commit-meta">
22
+ <span class="scheduled">expected to be deployed next</span>
23
+ </p>
24
+ <% end %>
20
25
  </div>
21
26
  <div class="commit-lock" >
22
27
  <%= link_to stack_commit_path(commit.stack, commit), class: 'action-lock-commit', data: {tooltip: t('commit.lock')} do %>
23
28
  <i class="icon icon--lock"></i>
24
29
  <% end %>
25
- <%= link_to stack_commit_path(commit.stack, commit), class: 'action-unlock-commit', data: {tooltip: t('commit.unlock'), confirm: t('commit.confirm_unlock')} do %>
30
+ <%= link_to stack_commit_path(commit.stack, commit), class: 'action-unlock-commit', data: {tooltip: unlock_commit_tooltip(commit), confirm: t('commit.confirm_unlock')} do %>
26
31
  <i class="icon icon--lock"></i>
27
32
  <% end %>
28
33
  </div>