shipit-engine 0.24.0 → 0.25.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/shipit/stacks.js.coffee +15 -1
  3. data/app/assets/stylesheets/_base/_icons.scss +18 -0
  4. data/app/assets/stylesheets/_base/_status-items.scss +28 -0
  5. data/app/assets/stylesheets/_pages/_commits.scss +1 -5
  6. data/app/assets/stylesheets/_pages/_deploy.scss +60 -3
  7. data/app/controllers/concerns/shipit/authentication.rb +1 -1
  8. data/app/controllers/shipit/merge_status_controller.rb +2 -0
  9. data/app/controllers/shipit/release_statuses_controller.rb +36 -0
  10. data/app/jobs/shipit/append_delayed_release_status_job.rb +17 -0
  11. data/app/jobs/shipit/cache_deploy_spec_job.rb +2 -0
  12. data/app/jobs/shipit/clear_git_cache_job.rb +1 -1
  13. data/app/jobs/shipit/create_release_statuses_job.rb +11 -0
  14. data/app/jobs/shipit/deferred_touch_job.rb +2 -0
  15. data/app/jobs/shipit/deliver_hook_job.rb +1 -0
  16. data/app/jobs/shipit/merge_pull_requests_job.rb +2 -0
  17. data/app/jobs/shipit/perform_commit_checks_job.rb +2 -0
  18. data/app/jobs/shipit/perform_task_job.rb +2 -4
  19. data/app/jobs/shipit/refresh_pull_request_job.rb +2 -0
  20. data/app/models/shipit/anonymous_user.rb +1 -1
  21. data/app/models/shipit/commit.rb +36 -3
  22. data/app/models/shipit/deploy.rb +43 -0
  23. data/app/models/shipit/deploy_spec.rb +16 -2
  24. data/app/models/shipit/deploy_spec/bundler_discovery.rb +5 -1
  25. data/app/models/shipit/deploy_spec/file_system.rb +4 -0
  26. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +1 -1
  27. data/app/models/shipit/ephemeral_commit_checks.rb +2 -4
  28. data/app/models/shipit/hook.rb +36 -3
  29. data/app/models/shipit/pull_request.rb +4 -2
  30. data/app/models/shipit/release_status.rb +41 -0
  31. data/app/models/shipit/rollback.rb +9 -0
  32. data/app/models/shipit/stack.rb +4 -9
  33. data/app/models/shipit/status/common.rb +4 -0
  34. data/app/models/shipit/status/group.rb +2 -1
  35. data/app/models/shipit/status/missing.rb +4 -0
  36. data/app/models/shipit/status/unknown.rb +15 -0
  37. data/app/models/shipit/task.rb +4 -0
  38. data/app/models/shipit/user.rb +16 -3
  39. data/app/serializers/shipit/stack_serializer.rb +1 -1
  40. data/app/views/shipit/deploys/_deploy.html.erb +18 -2
  41. data/config/locales/en.yml +3 -0
  42. data/config/routes.rb +2 -0
  43. data/db/migrate/20180802172632_allow_commit_without_author.rb +6 -0
  44. data/db/migrate/20180906083930_create_release_statuses.rb +21 -0
  45. data/lib/shipit.rb +5 -0
  46. data/lib/shipit/command.rb +14 -18
  47. data/lib/shipit/deploy_commands.rb +0 -4
  48. data/lib/shipit/engine.rb +1 -1
  49. data/lib/shipit/first_parent_commits_iterator.rb +1 -1
  50. data/lib/shipit/flock.rb +43 -0
  51. data/lib/shipit/github_app.rb +5 -3
  52. data/lib/shipit/rollback_commands.rb +6 -0
  53. data/lib/shipit/task_commands.rb +1 -5
  54. data/lib/shipit/version.rb +1 -1
  55. data/test/controllers/release_statuses_controller_test.rb +23 -0
  56. data/test/dummy/db/schema.rb +18 -3
  57. data/test/dummy/db/seeds.rb +4 -0
  58. data/test/fixtures/shipit/commits.yml +13 -0
  59. data/test/fixtures/shipit/release_statuses.yml +16 -0
  60. data/test/fixtures/shipit/stacks.yml +4 -0
  61. data/test/jobs/append_delayed_release_status_job_test.rb +25 -0
  62. data/test/jobs/cache_deploy_spec_job_test.rb +1 -2
  63. data/test/jobs/emit_event_job_test.rb +1 -1
  64. data/test/jobs/github_sync_job_test.rb +1 -0
  65. data/test/models/commits_test.rb +54 -1
  66. data/test/models/deploy_spec_test.rb +83 -11
  67. data/test/models/deploys_test.rb +52 -0
  68. data/test/models/hook_test.rb +1 -28
  69. data/test/models/pull_request_test.rb +19 -0
  70. data/test/models/release_statuses_test.rb +28 -0
  71. data/test/models/rollbacks_test.rb +2 -0
  72. data/test/models/stacks_test.rb +1 -1
  73. data/test/test_helper.rb +5 -0
  74. data/test/unit/rollback_commands_test.rb +35 -0
  75. metadata +121 -104
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ module Shipit
4
+ class AppendDelayedReleaseStatusJobTest < ActiveSupport::TestCase
5
+ setup do
6
+ @job = AppendDelayedReleaseStatusJob.new
7
+ @deploy = shipit_deploys(:shipit_complete)
8
+ end
9
+
10
+ test "#perform bails out if another status was appended in the meantime" do
11
+ cursor = @deploy.last_release_status
12
+ @deploy.append_release_status(cursor.state, 'Something else happened')
13
+ assert_no_difference -> { ReleaseStatus.count } do
14
+ @job.perform(@deploy, cursor: cursor, status: 'success', description: 'Nothing happened')
15
+ end
16
+ end
17
+
18
+ test "#perform appends the new status if no other status was appended in the meantime" do
19
+ cursor = @deploy.last_release_status
20
+ assert_difference -> { ReleaseStatus.count }, +1 do
21
+ @job.perform(@deploy, cursor: cursor, status: 'success', description: 'Something happened')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,11 +1,10 @@
1
1
  require 'test_helper'
2
- require 'tmpdir'
3
2
 
4
3
  module Shipit
5
4
  class CacheDeploySpecJobTest < ActiveSupport::TestCase
6
5
  setup do
7
6
  @stack = shipit_stacks(:shipit)
8
- @last_commit = shipit_commits(:fifth)
7
+ @last_commit = @stack.commits.last
9
8
  @job = CacheDeploySpecJob.new
10
9
  end
11
10
 
@@ -8,7 +8,7 @@ module Shipit
8
8
  end
9
9
 
10
10
  test "#perform schedule deliveries" do
11
- assert_difference -> { Delivery.scheduled.count }, 2 do
11
+ assert_enqueued_jobs(2, only: DeliverHookJob) do
12
12
  @job.perform(event: :deploy, stack_id: @stack.id, payload: {foo: 42}.to_json)
13
13
  end
14
14
  end
@@ -40,6 +40,7 @@ module Shipit
40
40
  @stack.deploys_and_rollbacks.destroy_all
41
41
 
42
42
  initial_queue = [
43
+ ["whoami", false],
43
44
  ["fix all the things", false],
44
45
  ["yoloshipit!", false],
45
46
  ["fix it!", false],
@@ -10,6 +10,59 @@ module Shipit
10
10
  @commit = shipit_commits(:first)
11
11
  end
12
12
 
13
+ test '.create_from_github handle unknown users' do
14
+ assert_difference -> { Commit.count }, +1 do
15
+ @stack.commits.create_from_github!(
16
+ resource(
17
+ sha: '2adaad1ad30c235d3a6e7981dfc1742f7ecb1e85',
18
+ author: {},
19
+ committer: {},
20
+ commit: {
21
+ author: {
22
+ name: 'George Abitbol',
23
+ email: '',
24
+ date: Time.now,
25
+ },
26
+ committer: {
27
+ name: 'George Abitbol',
28
+ email: '',
29
+ date: Time.now,
30
+ },
31
+ message: "commit to trigger staging build",
32
+ },
33
+ ),
34
+ )
35
+ end
36
+ end
37
+
38
+ test '.create_from_github handle PRs merged by another Shipit stacks' do
39
+ assert_difference -> { Commit.count }, +1 do
40
+ @stack.commits.create_from_github!(
41
+ resource(
42
+ sha: '2adaad1ad30c235d3a6e7981dfc1742f7ecb1e85',
43
+ author: {},
44
+ committer: {},
45
+ commit: {
46
+ author: {
47
+ name: 'Shipit',
48
+ email: '',
49
+ date: Time.now,
50
+ },
51
+ committer: {
52
+ name: 'Shipit',
53
+ email: '',
54
+ date: Time.now,
55
+ },
56
+ message: "commit to trigger staging build\n\nMerge-Requested-By: walrus\n",
57
+ },
58
+ ),
59
+ )
60
+ end
61
+
62
+ commit = Commit.last
63
+ assert_equal shipit_users(:walrus), commit.author
64
+ end
65
+
13
66
  test "#pull_request? detect pull request based on message format" do
14
67
  assert @pr.pull_request?
15
68
  refute @commit.pull_request?
@@ -181,7 +234,7 @@ module Shipit
181
234
  message: "more fish!",
182
235
  )
183
236
  @stack.reload
184
- assert_equal 2, @stack.undeployed_commits_count
237
+ assert_equal 3, @stack.undeployed_commits_count
185
238
  end
186
239
 
187
240
  test "fetch_stats! pulls additions and deletions from github" do
@@ -63,11 +63,11 @@ module Shipit
63
63
  test '#bundle_install return a sane default bundle install command' do
64
64
  @spec.stubs(:gemfile_lock_exists?).returns(true)
65
65
  command = %(
66
- bundle check --path=#{DeploySpec.bundle_path} ||
67
66
  bundle install
68
67
  --frozen
69
- --path=#{DeploySpec.bundle_path}
70
- --retry=2
68
+ --jobs 4
69
+ --path #{DeploySpec.bundle_path}
70
+ --retry 2
71
71
  --without=default:production:development:test:staging:benchmark:debug
72
72
  ).gsub(/\s+/, ' ').strip
73
73
  assert_equal command, @spec.bundle_install.last
@@ -77,11 +77,11 @@ module Shipit
77
77
  @spec.stubs(:gemfile_lock_exists?).returns(true)
78
78
  @spec.stubs(:load_config).returns('dependencies' => {'bundler' => {'without' => %w(some custom groups)}})
79
79
  command = %(
80
- bundle check --path=#{DeploySpec.bundle_path} ||
81
80
  bundle install
82
81
  --frozen
83
- --path=#{DeploySpec.bundle_path}
84
- --retry=2
82
+ --jobs 4
83
+ --path #{DeploySpec.bundle_path}
84
+ --retry 2
85
85
  --without=some:custom:groups
86
86
  ).gsub(/\s+/, ' ').strip
87
87
  assert_equal command, @spec.bundle_install.last
@@ -307,11 +307,24 @@ module Shipit
307
307
  'require' => [],
308
308
  'blocking' => [],
309
309
  },
310
- 'machine' => {'environment' => {}, 'directory' => nil, 'cleanup' => true},
310
+ 'machine' => {
311
+ 'environment' => {'BUNDLE_PATH' => @spec.bundle_path.to_s},
312
+ 'directory' => nil,
313
+ 'cleanup' => true,
314
+ },
311
315
  'review' => {'checklist' => [], 'monitoring' => [], 'checks' => []},
316
+ 'status' => {
317
+ 'context' => nil,
318
+ 'delay' => nil,
319
+ },
312
320
  'dependencies' => {'override' => []},
313
321
  'plugins' => {},
314
- 'deploy' => {'override' => nil, 'variables' => [], 'max_commits' => 8, 'interval' => 0},
322
+ 'deploy' => {
323
+ 'override' => nil,
324
+ 'variables' => [],
325
+ 'max_commits' => 8,
326
+ 'interval' => 0,
327
+ },
315
328
  'rollback' => {'override' => nil},
316
329
  'fetch' => nil,
317
330
  'tasks' => {},
@@ -350,9 +363,59 @@ module Shipit
350
363
  assert_equal ['foo'], definition.steps
351
364
  end
352
365
 
353
- test "#task_definitions returns kubernetes commands as well as comands from the config" do
366
+ test "#task_definitions will always have definitions from shipit.yml take precedence over other modules" do
367
+ # Create a subclass of `FileSystem` which we can include another module in without
368
+ # affecting any other tests
369
+ DuplicateCustomizedDeploySpec = Class.new(Shipit::DeploySpec::FileSystem)
370
+
371
+ # Create the module we want to include in our new class
372
+ # For this test case, we want to have the module create a task with the same
373
+ # ID as defined from config
374
+ module TestTaskDiscovery
375
+ def discover_task_definitions
376
+ {
377
+ 'config_task' => {'steps' => %w(bar)},
378
+ }.merge!(super)
379
+ end
380
+ end
381
+
382
+ # Include the module in our new test class
383
+ DuplicateCustomizedDeploySpec.include TestTaskDiscovery
384
+
385
+ # Setup the spec as we would normally, but use the customized version
386
+ @spec = DuplicateCustomizedDeploySpec.new(@app_dir, 'env')
387
+ @spec.stubs(:load_config).returns(
388
+ 'tasks' => {'config_task' => {'steps' => %w(foo)}},
389
+ )
390
+ tasks = @spec.task_definitions
391
+
392
+ # Assert we get only the task from the config, not from the module
393
+ assert_equal %w(config_task), tasks.map(&:id)
394
+ assert_equal ["foo"], tasks.first.steps
395
+ end
396
+
397
+ test "#task_definitions returns comands from the config and other modules" do
398
+ # Create a subclass of `FileSystem` which we can include another module in without
399
+ # affecting any other tests
400
+ CustomizedDeploySpec = Class.new(Shipit::DeploySpec::FileSystem)
401
+
402
+ # Create the module we want to include in our new class
403
+ # This module demonstrates how to have new tasks to be appended to the task list
404
+ module TestTaskDiscovery
405
+ def discover_task_definitions
406
+ {
407
+ 'module_task' => {'steps' => %w(bar)},
408
+ }.merge(super)
409
+ end
410
+ end
411
+
412
+ # Include the module in our new test class
413
+ CustomizedDeploySpec.include TestTaskDiscovery
414
+
415
+ # Setup the spec as we would normally, but use the customized version
416
+ @spec = CustomizedDeploySpec.new(@app_dir, 'env')
354
417
  @spec.stubs(:load_config).returns(
355
- 'tasks' => {'another_task' => {'steps' => %w(foo)}},
418
+ 'tasks' => {'config_task' => {'steps' => %w(foo)}},
356
419
  'kubernetes' => {
357
420
  'namespace' => 'foo',
358
421
  'context' => 'bar',
@@ -360,7 +423,16 @@ module Shipit
360
423
  },
361
424
  )
362
425
  tasks = @spec.task_definitions
363
- assert_equal 2, tasks.size
426
+
427
+ # Assert we get tasks from all three sources: config, shipit-engine defined modules, and
428
+ # "third party" modules
429
+ assert_equal %w(config_task module_task restart), tasks.map(&:id).sort
430
+
431
+ module_task = tasks.find { |t| t.id == "config_task" }
432
+ assert_equal ["foo"], module_task.steps
433
+
434
+ config_task = tasks.find { |t| t.id == "module_task" }
435
+ assert_equal ["bar"], config_task.steps
364
436
 
365
437
  restart_task = tasks.find { |t| t.id == "restart" }
366
438
  assert_equal ["kubernetes-restart foo bar --max-watch-seconds 1200"], restart_task.steps
@@ -466,6 +466,58 @@ module Shipit
466
466
  assert_predicate @deploy, :alive?
467
467
  end
468
468
 
469
+ test "triggering a deploy sets the release status as pending" do
470
+ @commit = shipit_commits(:fifth)
471
+ assert_difference -> { ReleaseStatus.count }, +1 do
472
+ assert_equal 'unknown', @commit.last_release_status.state
473
+ @deploy = @stack.trigger_deploy(@commit, AnonymousUser.new)
474
+ assert_equal 'pending', @commit.last_release_status.state
475
+ end
476
+ end
477
+
478
+ test "failing a deploy sets the release status as error" do
479
+ @deploy = shipit_deploys(:shipit_running)
480
+ assert_difference -> { ReleaseStatus.count }, +1 do
481
+ assert_not_equal 'error', @deploy.last_release_status.state
482
+ @deploy.report_failure!(StandardError.new)
483
+ assert_equal 'error', @deploy.last_release_status.state
484
+ end
485
+ end
486
+
487
+ test "succeeding a deploy sets the release status as success if the status delay is 0s" do
488
+ @deploy = shipit_deploys(:shipit_running)
489
+ @deploy.stack.expects(:release_status_delay).returns(Duration.parse(0))
490
+
491
+ assert_difference -> { ReleaseStatus.count }, +1 do
492
+ assert_not_equal 'success', @deploy.last_release_status.state
493
+ @deploy.complete!
494
+ assert_equal 'success', @deploy.last_release_status.state
495
+ end
496
+ end
497
+
498
+ test "succeeding a deploy sets the release status as pending if the status delay is longer than 0s" do
499
+ @deploy = shipit_deploys(:shipit_running)
500
+ @deploy.stack.expects(:release_status_delay).returns(Duration.parse(1))
501
+
502
+ assert_difference -> { ReleaseStatus.count }, +1 do
503
+ assert_not_equal 'success', @deploy.last_release_status.state
504
+ assert_enqueued_with(job: AppendDelayedReleaseStatusJob) do
505
+ @deploy.complete!
506
+ end
507
+ assert_equal 'pending', @deploy.last_release_status.state
508
+ end
509
+ end
510
+
511
+ test "triggering a rollback sets the release status as failure" do
512
+ @deploy = shipit_deploys(:shipit_complete)
513
+
514
+ assert_difference -> { ReleaseStatus.count }, +1 do
515
+ assert_not_equal 'failure', @deploy.last_release_status.state
516
+ @deploy.trigger_rollback
517
+ assert_equal 'failure', @deploy.reload.last_release_status.state
518
+ end
519
+ end
520
+
469
521
  private
470
522
 
471
523
  def expect_event(deploy)
@@ -29,23 +29,10 @@ module Shipit
29
29
  assert_equal ["Events is not a strict subset of #{Hook::EVENTS.inspect}"], @hook.errors.full_messages
30
30
  end
31
31
 
32
- test ".emit enqueues an EmitEventJob with the proper payload" do
33
- assert_enqueued_with(job: EmitEventJob) do
34
- Hook.emit(:deploy, @stack, foo: 42)
35
- end
36
- end
37
-
38
32
  test ".deliver schedules a delivery for each matching hook" do
39
- assert_difference -> { Delivery.count }, 2 do
33
+ assert_enqueued_jobs(2, only: DeliverHookJob) do
40
34
  Hook.deliver(:deploy, @stack, 'foo' => 42)
41
35
  end
42
-
43
- delivery = Delivery.last
44
-
45
- assert_equal @hook.delivery_url, delivery.url
46
- assert_equal 'application/x-www-form-urlencoded', delivery.content_type
47
- assert_equal 'foo=42', delivery.payload
48
- assert_equal 'scheduled', delivery.status
49
36
  end
50
37
 
51
38
  test ".scoped? returns true if the hook has a stack_id" do
@@ -55,19 +42,5 @@ module Shipit
55
42
  @hook.stack_id = 42
56
43
  assert @hook.scoped?
57
44
  end
58
-
59
- test "#purge_old_deliveries!" do
60
- Hook.deliver(:deploy, @stack, 'foo' => 42)
61
- @hook.deliveries.update_all(status: 'sent')
62
-
63
- previous_ids = @hook.deliveries.sent.order(id: :desc).pluck(:id)
64
-
65
- assert_difference -> { @hook.deliveries.sent.count }, -1 do
66
- @hook.purge_old_deliveries!(keep: 1)
67
- end
68
-
69
- after_ids = @hook.deliveries.sent.order(id: :desc).pluck(:id)
70
- assert_equal previous_ids[0..-2], after_ids
71
- end
72
45
  end
73
46
  end
@@ -183,6 +183,25 @@ module Shipit
183
183
  refute_predicate @pr, :rejected?
184
184
  end
185
185
 
186
+ test "#reject_unless_mergeable! reject the PR if it has a missing CI status" do
187
+ @pr.head.statuses.where(context: 'ci/circle').delete_all
188
+
189
+ assert_predicate @pr, :all_status_checks_passed?
190
+ refute_predicate @pr, :any_status_checks_failed?
191
+ assert_equal false, @pr.reject_unless_mergeable!
192
+ refute_predicate @pr, :rejected?
193
+ end
194
+
195
+ test "#reject_unless_mergeable! reject the PR if it has a missing CI status (multi-status)" do
196
+ @pr.head.statuses.where(context: 'ci/circle').delete_all
197
+ @pr.head.statuses.create!(stack: @pr.stack, state: 'success', context: 'ci/travis')
198
+
199
+ assert_predicate @pr, :all_status_checks_passed?
200
+ refute_predicate @pr, :any_status_checks_failed?
201
+ assert_equal false, @pr.reject_unless_mergeable!
202
+ refute_predicate @pr, :rejected?
203
+ end
204
+
186
205
  test "#reject_unless_mergeable! rejects the PR if it is stale" do
187
206
  @pr.stubs(:stale?).returns(true)
188
207
  assert_equal true, @pr.reject_unless_mergeable!
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ module Shipit
4
+ class ReleaseStatusesTest < ActiveSupport::TestCase
5
+ test "#create_status_on_github! calls GitHub API" do
6
+ Shipit.github.api.expects(:create_status).once.with(
7
+ 'shopify/shipit-engine',
8
+ shipit_commits(:fourth).sha,
9
+ 'pending',
10
+ context: 'shipit/production',
11
+ target_url: 'https://example.com/deploys/42',
12
+ description: 'Deploy started',
13
+ ).returns(resource(id: 42))
14
+
15
+ @status = shipit_release_statuses(:to_be_created)
16
+ assert_nil @status.github_id
17
+ @status.create_status_on_github!
18
+ assert_equal 42, @status.github_id
19
+ end
20
+
21
+ test "#create_status_on_github! does nothing if the github_id is alreayd recorded" do
22
+ Shipit.github.api.expects(:create_status).never
23
+
24
+ @status = shipit_release_statuses(:created)
25
+ @status.create_status_on_github!
26
+ end
27
+ end
28
+ end
@@ -36,6 +36,7 @@ module Shipit
36
36
 
37
37
  expected = [
38
38
  ['Revert "Merge pull request #7 from shipit-engine/yoloshipit"', false],
39
+ ["whoami", false],
39
40
  ['fix all the things', false],
40
41
  ]
41
42
  assert_equal expected, @stack.undeployed_commits.map { |c| [c.title, c.locked?] }
@@ -46,6 +47,7 @@ module Shipit
46
47
 
47
48
  expected = [
48
49
  ['Revert "Merge pull request #7 from shipit-engine/yoloshipit"', false],
50
+ ["whoami", true],
49
51
  ['fix all the things', true],
50
52
  ['yoloshipit!', true],
51
53
  ]
@@ -354,7 +354,7 @@ module Shipit
354
354
 
355
355
  test "the git cache lock prevent concurrent access to the git cache" do
356
356
  @stack.acquire_git_cache_lock do
357
- assert_raises Redis::Lock::LockTimeout do
357
+ assert_raises Flock::TimeoutError do
358
358
  @stack.acquire_git_cache_lock(timeout: 0.1) {}
359
359
  end
360
360
  end