shipit-engine 0.27.1 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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>