shipit-engine 0.24.0 → 0.25.0

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