shipit-engine 0.15.0 → 0.16.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/app/assets/javascripts/shipit/page_updater.js.coffee +63 -0
  4. data/app/assets/javascripts/shipit/stacks.js.coffee +9 -21
  5. data/app/assets/stylesheets/_base/_base.scss +2 -2
  6. data/app/assets/stylesheets/_base/_colors.scss +0 -1
  7. data/app/assets/stylesheets/_base/_forms.scss +14 -0
  8. data/app/assets/stylesheets/_pages/_commits.scss +16 -6
  9. data/app/assets/stylesheets/_pages/_settings.scss +8 -0
  10. data/app/assets/stylesheets/_pages/_stacks.scss +1 -1
  11. data/app/controllers/shipit/api/base_controller.rb +7 -3
  12. data/app/controllers/shipit/api/ccmenu_controller.rb +33 -0
  13. data/app/controllers/shipit/api/pull_requests_controller.rb +36 -0
  14. data/app/controllers/shipit/api/stacks_controller.rb +1 -0
  15. data/app/controllers/shipit/ccmenu_url_controller.rb +22 -0
  16. data/app/controllers/shipit/pull_requests_controller.rb +30 -0
  17. data/app/controllers/shipit/stacks_controller.rb +7 -2
  18. data/app/controllers/shipit/webhooks_controller.rb +1 -2
  19. data/app/helpers/shipit/github_url_helper.rb +8 -2
  20. data/app/helpers/shipit/shipit_helper.rb +9 -0
  21. data/app/helpers/shipit/stacks_helper.rb +22 -7
  22. data/app/jobs/shipit/background_job/unique.rb +19 -1
  23. data/app/jobs/shipit/cache_deploy_spec_job.rb +1 -1
  24. data/app/jobs/shipit/merge_pull_requests_job.rb +26 -0
  25. data/app/jobs/shipit/perform_task_job.rb +1 -1
  26. data/app/jobs/shipit/refresh_pull_request_job.rb +8 -0
  27. data/app/models/concerns/shipit/deferred_touch.rb +6 -1
  28. data/app/models/shipit/anonymous_user.rb +4 -0
  29. data/app/models/shipit/application_record.rb +5 -0
  30. data/app/models/shipit/commit.rb +51 -49
  31. data/app/models/shipit/commit_message.rb +32 -0
  32. data/app/models/shipit/deploy.rb +5 -0
  33. data/app/models/shipit/deploy_spec.rb +26 -1
  34. data/app/models/shipit/deploy_spec/file_system.rb +6 -1
  35. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +10 -13
  36. data/app/models/shipit/deploy_spec/npm_discovery.rb +2 -1
  37. data/app/models/shipit/duration.rb +3 -1
  38. data/app/models/shipit/hook.rb +1 -0
  39. data/app/models/shipit/pull_request.rb +252 -0
  40. data/app/models/shipit/stack.rb +33 -17
  41. data/app/models/shipit/status.rb +1 -16
  42. data/app/models/shipit/status/common.rb +45 -0
  43. data/app/models/shipit/status/group.rb +82 -0
  44. data/app/models/shipit/status/missing.rb +30 -0
  45. data/app/models/shipit/status/unknown.rb +33 -0
  46. data/app/models/shipit/unlimited_api_client.rb +10 -0
  47. data/app/serializers/shipit/commit_serializer.rb +1 -1
  48. data/app/serializers/shipit/pull_request_serializer.rb +20 -0
  49. data/app/serializers/shipit/stack_serializer.rb +6 -2
  50. data/app/views/layouts/shipit.html.erb +41 -39
  51. data/app/views/shipit/ccmenu/project.xml.builder +13 -0
  52. data/app/views/shipit/commits/_commit.html.erb +1 -1
  53. data/app/views/shipit/deploys/_deploy.html.erb +1 -1
  54. data/app/views/shipit/pull_requests/_pull_request.html.erb +29 -0
  55. data/app/views/shipit/pull_requests/index.html.erb +20 -0
  56. data/app/views/shipit/shared/_author.html.erb +7 -0
  57. data/app/views/shipit/stacks/_header.html.erb +5 -0
  58. data/app/views/shipit/stacks/settings.html.erb +13 -0
  59. data/app/views/shipit/stacks/show.html.erb +3 -2
  60. data/app/views/shipit/statuses/_group.html.erb +1 -1
  61. data/app/views/shipit/tasks/_task.html.erb +1 -1
  62. data/config/initializers/inflections.rb +3 -0
  63. data/config/locales/en.yml +1 -3
  64. data/config/routes.rb +8 -0
  65. data/db/migrate/20170130113633_create_shipit_pull_requests.rb +25 -0
  66. data/db/migrate/20170208143657_add_pull_request_number_and_title_to_commits.rb +7 -0
  67. data/db/migrate/20170208154609_backfill_merge_commits.rb +13 -0
  68. data/db/migrate/20170209160355_add_branch_to_pull_requests.rb +5 -0
  69. data/db/migrate/20170215123538_add_merge_queue_enabled_to_stacks.rb +5 -0
  70. data/db/migrate/20170220152410_improve_users_indexing.rb +6 -0
  71. data/db/migrate/20170221102128_improve_tasks_indexing.rb +8 -0
  72. data/db/migrate/20170221130336_add_last_revalidated_at_on_pull_requests.rb +10 -0
  73. data/lib/shipit.rb +2 -0
  74. data/lib/shipit/version.rb +1 -1
  75. data/lib/tasks/cron.rake +1 -0
  76. data/test/controllers/api/ccmenu_controller_test.rb +57 -0
  77. data/test/controllers/api/commits_controller_test.rb +1 -1
  78. data/test/controllers/api/pull_requests_controller_test.rb +59 -0
  79. data/test/controllers/ccmenu_controller_test.rb +33 -0
  80. data/test/controllers/pull_requests_controller_test.rb +31 -0
  81. data/test/controllers/webhooks_controller_test.rb +3 -4
  82. data/test/dummy/config/environments/development.rb +3 -1
  83. data/test/dummy/data/stacks/shopify/junk/production/git/README.md +8 -0
  84. data/test/dummy/data/stacks/shopify/junk/production/git/circle.yml +4 -0
  85. data/test/dummy/data/stacks/shopify/junk/production/git/shipit.yml +4 -0
  86. data/test/dummy/db/development.sqlite3 +0 -0
  87. data/test/dummy/db/schema.rb +45 -11
  88. data/test/dummy/db/seeds.rb +33 -10
  89. data/test/dummy/db/test.sqlite3 +0 -0
  90. data/test/fixtures/shipit/commits.yml +14 -0
  91. data/test/fixtures/shipit/pull_requests.yml +56 -0
  92. data/test/fixtures/shipit/stacks.yml +5 -1
  93. data/test/fixtures/shipit/statuses.yml +8 -0
  94. data/test/helpers/json_helper.rb +16 -14
  95. data/test/jobs/merge_pull_requests_job_test.rb +59 -0
  96. data/test/models/commits_test.rb +104 -49
  97. data/test/{unit → models}/deploy_spec_test.rb +138 -12
  98. data/test/models/deploys_test.rb +10 -4
  99. data/test/models/pull_request_test.rb +197 -0
  100. data/test/models/stacks_test.rb +46 -53
  101. data/test/models/status/group_test.rb +44 -0
  102. data/test/models/status/missing_test.rb +23 -0
  103. data/test/models/status_test.rb +3 -6
  104. data/test/unit/csv_serializer_test.rb +10 -2
  105. metadata +57 -12
  106. data/app/models/shipit/missing_status.rb +0 -21
  107. data/app/models/shipit/status_group.rb +0 -35
  108. data/app/models/shipit/unknown_status.rb +0 -48
  109. data/app/views/shipit/commits/_commit_author.html.erb +0 -7
  110. data/test/models/missing_status_test.rb +0 -23
  111. data/test/models/status_group_test.rb +0 -26
@@ -30,11 +30,6 @@ module Shipit
30
30
  assert_nil @commit.pull_request_title
31
31
  end
32
32
 
33
- test "#pull_request_url build the pull request url from the message" do
34
- assert_equal 'https://github.com/shopify/shipit-engine/pull/31', @pr.pull_request_url
35
- assert_nil @commit.pull_request_url
36
- end
37
-
38
33
  test "#newer_than(nil) returns all commits" do
39
34
  assert_equal @stack.commits.all.to_a, @stack.commits.newer_than(nil).to_a
40
35
  end
@@ -254,29 +249,19 @@ module Shipit
254
249
  assert_equal 'pending', @commit.reload.state
255
250
  end
256
251
 
257
- test "#last_statuses returns the list of the most recent status of each context" do
258
- assert_equal 4, shipit_commits(:second).statuses.count
259
- assert_equal 2, shipit_commits(:second).last_statuses.count
260
- end
261
-
262
- test "#last_statuses returns [UnknownStatus] if the commit has no statuses" do
263
- commit = shipit_commits(:second)
264
- commit.statuses = []
265
- assert_equal UnknownStatus.new(commit), commit.significant_status
266
- end
267
-
268
- test "#status returns UnknownStatus if the commit has no status" do
252
+ test "#status returns an unknown if the commit has no statuses" do
269
253
  commit = shipit_commits(:second)
270
254
  commit.statuses = []
271
- assert_equal UnknownStatus.new(commit), commit.status
255
+ assert_predicate commit.status, :unknown?
272
256
  end
273
257
 
274
- test "#visible_statuses rejects the statuses that are specified in the deploy spec's `ci.hide`" do
258
+ test "#status rejects the statuses that are specified in the deploy spec's `ci.hide`" do
275
259
  commit = shipit_commits(:second)
276
- assert_equal 2, commit.visible_statuses.count
260
+ assert_predicate commit.status, :group?
261
+ assert_equal 2, commit.status.size
277
262
  commit.stack.update!(cached_deploy_spec: DeploySpec.new('ci' => {'hide' => 'metrics/coveralls'}))
278
263
  commit.reload
279
- assert_equal 1, commit.visible_statuses.size
264
+ refute_predicate commit.status, :group?
280
265
  end
281
266
 
282
267
  test "#deployable? is true if commit status is 'success'" do
@@ -343,8 +328,10 @@ module Shipit
343
328
  end
344
329
 
345
330
  test "#add_status does not fire webhooks for invisible statuses" do
331
+ @stack.deploys_and_rollbacks.destroy_all
346
332
  commit = shipit_commits(:second)
347
333
  assert commit.stack.hooks.where(events: ['commit_status']).size >= 1
334
+ refute_predicate commit, :deployed?
348
335
 
349
336
  expect_no_hook(:deployable_status) do
350
337
  github_status = OpenStruct.new(
@@ -358,8 +345,10 @@ module Shipit
358
345
  end
359
346
 
360
347
  test "#add_status does not fire webhooks for non-meaningful statuses" do
348
+ @stack.deploys_and_rollbacks.destroy_all
361
349
  commit = shipit_commits(:second)
362
350
  assert commit.stack.hooks.where(events: ['commit_status']).size >= 1
351
+ refute_predicate commit, :deployed?
363
352
 
364
353
  expect_no_hook(:deployable_status) do
365
354
  github_status = OpenStruct.new(
@@ -372,27 +361,38 @@ module Shipit
372
361
  end
373
362
  end
374
363
 
375
- test "#visible_statuses forward the last_statuses to the stack" do
364
+ test "#add_status does not fire webhooks for already deployed commits" do
376
365
  commit = shipit_commits(:second)
377
- stack = commit.stack
378
- stack.expects(:filter_visible_statuses).with(commit.last_statuses)
379
- commit.visible_statuses
366
+ assert_predicate commit, :deployed?
367
+
368
+ expect_no_hook(:deployable_status) do
369
+ github_status = OpenStruct.new(
370
+ state: 'failure',
371
+ description: 'Sad',
372
+ context: 'ci/travis',
373
+ created_at: 1.day.ago.to_s(:db),
374
+ )
375
+ commit.create_status_from_github!(github_status)
376
+ end
380
377
  end
381
378
 
382
- test "#meaningful_statuses forward the last_statuses to the stack" do
379
+ test "#add_status schedule a MergePullRequests job if the commit transition to `pending` or `success`" do
383
380
  commit = shipit_commits(:second)
384
- stack = commit.stack
385
- stack.expects(:filter_meaningful_statuses).with(commit.last_statuses)
386
- commit.meaningful_statuses
387
- end
381
+ github_status = OpenStruct.new(
382
+ state: 'success',
383
+ description: 'Cool',
384
+ context: 'metrics/coveralls',
385
+ created_at: 1.day.ago.to_s(:db),
386
+ )
388
387
 
389
- test "#significant_status is UnknownStatus when the commit has no statuses" do
390
- commit = shipit_commits(:first)
391
- commit.statuses = []
392
- assert_equal UnknownStatus.new(commit), commit.significant_status
388
+ assert_equal 'failure', commit.state
389
+ assert_enqueued_with(job: MergePullRequestsJob, args: [@commit.stack]) do
390
+ commit.create_status_from_github!(github_status)
391
+ assert_equal 'success', commit.state
392
+ end
393
393
  end
394
394
 
395
- test "#significant_status hierarchy uses failures and errors, then pending, then successes, then UnknownStatus" do
395
+ test "#status hierarchy uses failures and errors, then pending, then successes, then Status::Unknown" do
396
396
  commit = shipit_commits(:first)
397
397
  pending = commit.statuses.new(stack_id: @stack.id, state: 'pending', context: 'ci/pending')
398
398
  failure = commit.statuses.new(stack_id: @stack.id, state: 'failure', context: 'ci/failure')
@@ -400,37 +400,92 @@ module Shipit
400
400
  success = commit.statuses.new(stack_id: @stack.id, state: 'success', context: 'ci/success')
401
401
 
402
402
  commit.reload.statuses = [pending, failure, success, error]
403
- assert_includes [error, failure], commit.significant_status
403
+ assert_equal 'error', commit.status.state
404
404
 
405
405
  commit.reload.statuses = [pending, failure, success]
406
- assert_equal failure, commit.significant_status
406
+ assert_equal 'failure', commit.status.state
407
407
 
408
408
  commit.reload.statuses = [pending, error, success]
409
- assert_equal error, commit.significant_status
409
+ assert_equal 'error', commit.status.state
410
410
 
411
411
  commit.reload.statuses = [success, pending]
412
- assert_equal pending, commit.significant_status
412
+ assert_equal 'pending', commit.status.state
413
413
 
414
414
  commit.reload.statuses = [success]
415
- assert_equal success, commit.significant_status
415
+ assert_equal 'success', commit.status.state
416
416
 
417
417
  commit.reload.statuses = []
418
- assert_equal UnknownStatus.new(commit), commit.significant_status
418
+ assert_equal 'unknown', commit.status.state
419
419
  end
420
420
 
421
- test "#significant_status is UnknownStatus when the commit has statuses but none meaningful" do
422
- commit = shipit_commits(:first)
423
- commit.stubs(meaningful_statuses: [])
424
- assert_equal UnknownStatus.new(commit), commit.significant_status
421
+ test "merge commits are linked to the matching Pull Request if there is one" do
422
+ commit = @stack.commits.create!(
423
+ author: shipit_users(:shipit),
424
+ authored_at: Time.now,
425
+ committer: shipit_users(:shipit),
426
+ committed_at: Time.now,
427
+ sha: '5590fd8b5f2be05d1fedb763a3605ee461c39074',
428
+ message: "Merge pull request #62 from shipit-engine/yoloshipit\n\nyoloshipit!",
429
+ )
430
+ pull_request = shipit_pull_requests(:shipit_pending)
431
+
432
+ assert_predicate commit, :pull_request?
433
+ assert_equal 62, commit.pull_request_number
434
+ assert_equal pull_request.title, commit.pull_request_title
435
+ assert_equal pull_request, commit.pull_request
436
+ end
437
+
438
+ test "merge commits infer pull request number and title from the message if it's not a known pull request" do
439
+ commit = @stack.commits.create!(
440
+ author: shipit_users(:shipit),
441
+ authored_at: Time.now,
442
+ committer: shipit_users(:shipit),
443
+ committed_at: Time.now,
444
+ sha: '5590fd8b5f2be05d1fedb763a3605ee461c39074',
445
+ message: "Merge pull request #99 from shipit-engine/yoloshipit\n\nyoloshipit!",
446
+ )
447
+
448
+ assert_predicate commit, :pull_request?
449
+ assert_equal 99, commit.pull_request_number
450
+ assert_equal 'yoloshipit!', commit.pull_request_title
451
+ assert_nil commit.pull_request
452
+ end
453
+
454
+ test "the merge requester if known overrides the commit author" do
455
+ commit = @stack.commits.create!(
456
+ author: shipit_users(:shipit),
457
+ authored_at: Time.now,
458
+ committer: shipit_users(:shipit),
459
+ committed_at: Time.now,
460
+ sha: '5590fd8b5f2be05d1fedb763a3605ee461c39074',
461
+ message: "Merge pull request #62 from shipit-engine/yoloshipit\n\nyoloshipit!",
462
+ )
463
+
464
+ assert_equal shipit_users(:walrus), commit.author
465
+ end
466
+
467
+ test "#pull_request_number and #pull_request_title are nil if the message is not a merge commit message" do
468
+ commit = @stack.commits.create!(
469
+ author: shipit_users(:shipit),
470
+ authored_at: Time.now,
471
+ committer: shipit_users(:shipit),
472
+ committed_at: Time.now,
473
+ sha: '5590fd8b5f2be05d1fedb763a3605ee461c39074',
474
+ message: "Yoloshipit!",
475
+ )
476
+
477
+ refute_predicate commit, :pull_request?
478
+ assert_nil commit.pull_request_number
479
+ assert_nil commit.pull_request_title
480
+ assert_nil commit.pull_request
425
481
  end
426
482
 
427
483
  private
428
484
 
429
485
  def expect_event(stack)
430
- Pubsubstub::RedisPubSub.expects(:publish).at_least_once
431
- Pubsubstub::RedisPubSub.expects(:publish).with do |channel, event|
432
- data = JSON.load(event.data)
433
- channel == "stack.#{stack.id}" && data['url'] == "/#{stack.to_param}"
486
+ Pubsubstub.expects(:publish).at_least_once
487
+ Pubsubstub.expects(:publish).with do |channel, _payload, _options = {}|
488
+ channel == "stack.#{stack.id}"
434
489
  end
435
490
  end
436
491
 
@@ -126,6 +126,29 @@ module Shipit
126
126
  assert_equal ["kubernetes-deploy foo bar"], @spec.deploy_steps
127
127
  end
128
128
 
129
+ test "#deploy_steps returns kubernetes-deploy command if both capfile and `kubernetes` are present" do
130
+ @spec.stubs(:bundler?).returns(true)
131
+ @spec.stubs(:capistrano?).returns(true)
132
+ @spec.stubs(:load_config).returns(
133
+ 'kubernetes' => {
134
+ 'namespace' => 'foo',
135
+ 'context' => 'bar',
136
+ },
137
+ )
138
+ assert_equal ["kubernetes-deploy foo bar"], @spec.deploy_steps
139
+ end
140
+
141
+ test "#deploy_steps returns kubernetes command if `kubernetes` is present and template_dir is set" do
142
+ @spec.stubs(:load_config).returns(
143
+ 'kubernetes' => {
144
+ 'namespace' => 'foo',
145
+ 'context' => 'bar',
146
+ 'template_dir' => 'k8s_templates/',
147
+ },
148
+ )
149
+ assert_equal ["kubernetes-deploy --template-dir k8s_templates/ foo bar"], @spec.deploy_steps
150
+ end
151
+
129
152
  test "#deploy_steps prepend and append pre and post steps" do
130
153
  @spec.stubs(:load_config).returns('deploy' => {'pre' => ['before'], 'post' => ['after']})
131
154
  @spec.expects(:bundler?).returns(true).at_least_once
@@ -168,20 +191,21 @@ module Shipit
168
191
  assert_equal ["kubernetes-deploy foo bar"], @spec.rollback_steps
169
192
  end
170
193
 
171
- test '#machine_env returns an environment hash' do
172
- @spec.stubs(:load_config).returns('machine' => {'environment' => {'GLOBAL' => '1'}})
173
- assert_equal({'GLOBAL' => '1'}, @spec.machine_env)
174
- end
175
-
176
- test '#discover_machine_env contains K8S_TEMPLATE_FOLDER if `kubernetes.template_dir` is present' do
194
+ test "#rollback_steps returns kubernetes-deploy command when both capfile and `kubernetes` are present" do
195
+ @spec.stubs(:bundler?).returns(true)
196
+ @spec.stubs(:capistrano?).returns(true)
177
197
  @spec.stubs(:load_config).returns(
178
198
  'kubernetes' => {
179
199
  'namespace' => 'foo',
180
200
  'context' => 'bar',
181
- 'template_dir' => '/egg/spam',
182
201
  },
183
202
  )
184
- assert_equal '/egg/spam', @spec.discover_machine_env['K8S_TEMPLATE_FOLDER']
203
+ assert_equal ["kubernetes-deploy foo bar"], @spec.rollback_steps
204
+ end
205
+
206
+ test '#machine_env returns an environment hash' do
207
+ @spec.stubs(:load_config).returns('machine' => {'environment' => {'GLOBAL' => '1'}})
208
+ assert_equal({'GLOBAL' => '1'}, @spec.machine_env)
185
209
  end
186
210
 
187
211
  test '#load_config can grab the env-specific shipit.yml file' do
@@ -256,12 +280,21 @@ module Shipit
256
280
  assert_instance_of DeploySpec::FileSystem, @spec
257
281
  assert_instance_of DeploySpec, @spec.cacheable
258
282
  config = {
259
- 'ci' => {'hide' => [], 'allow_failures' => [], 'require' => []},
283
+ 'merge' => {
284
+ 'require' => [],
285
+ 'ignore' => [],
286
+ 'revalidate_after' => nil,
287
+ },
288
+ 'ci' => {
289
+ 'hide' => [],
290
+ 'allow_failures' => [],
291
+ 'require' => [],
292
+ },
260
293
  'machine' => {'environment' => {}, 'directory' => nil, 'cleanup' => true},
261
294
  'review' => {'checklist' => [], 'monitoring' => [], 'checks' => []},
262
295
  'dependencies' => {'override' => []},
263
296
  'plugins' => {},
264
- 'deploy' => {'override' => nil, 'variables' => [], 'max_commits' => nil, 'interval' => 0},
297
+ 'deploy' => {'override' => nil, 'variables' => [], 'max_commits' => 8, 'interval' => 0},
265
298
  'rollback' => {'override' => nil},
266
299
  'fetch' => nil,
267
300
  'tasks' => {},
@@ -380,6 +413,99 @@ module Shipit
380
413
  assert_equal %w(ci/circleci ci/jenkins), @spec.hidden_statuses
381
414
  end
382
415
 
416
+ test "pull_request_ignored_statuses defaults to the union of ci.hide and ci.allow_failures" do
417
+ @spec.expects(:load_config).returns(
418
+ 'ci' => {
419
+ 'hide' => %w(ci/circleci ci/jenkins),
420
+ 'allow_failures' => %w(ci/circleci ci/travis),
421
+ },
422
+ )
423
+ assert_equal %w(ci/circleci ci/jenkins ci/travis).sort, @spec.pull_request_ignored_statuses.sort
424
+ end
425
+
426
+ test "pull_request_ignored_statuses defaults to empty if `merge.require` is present" do
427
+ @spec.expects(:load_config).returns(
428
+ 'merge' => {
429
+ 'require' => 'bar',
430
+ },
431
+ 'ci' => {
432
+ 'hide' => %w(ci/circleci ci/jenkins),
433
+ 'allow_failures' => %w(ci/circleci ci/travis),
434
+ },
435
+ )
436
+ assert_equal [], @spec.pull_request_ignored_statuses
437
+ end
438
+
439
+ test "pull_request_ignored_statuses returns `merge.ignore` if present" do
440
+ @spec.expects(:load_config).returns(
441
+ 'merge' => {
442
+ 'ignore' => 'bar',
443
+ },
444
+ 'ci' => {
445
+ 'hide' => %w(ci/circleci ci/jenkins),
446
+ 'allow_failures' => %w(ci/circleci ci/travis),
447
+ },
448
+ )
449
+ assert_equal ['bar'], @spec.pull_request_ignored_statuses
450
+ end
451
+
452
+ test "pull_request_required_statuses defaults to ci.require" do
453
+ @spec.expects(:load_config).returns(
454
+ 'ci' => {
455
+ 'require' => %w(ci/circleci ci/jenkins),
456
+ },
457
+ )
458
+ assert_equal %w(ci/circleci ci/jenkins), @spec.pull_request_required_statuses
459
+ end
460
+
461
+ test "pull_request_required_statuses defaults to empty if `merge.ignore` is present" do
462
+ @spec.expects(:load_config).returns(
463
+ 'merge' => {
464
+ 'ignore' => 'bar',
465
+ },
466
+ 'ci' => {
467
+ 'require' => %w(ci/circleci ci/jenkins),
468
+ },
469
+ )
470
+ assert_equal [], @spec.pull_request_required_statuses
471
+ end
472
+
473
+ test "pull_request_required_statuses returns `merge.require` if present" do
474
+ @spec.expects(:load_config).returns(
475
+ 'merge' => {
476
+ 'require' => 'bar',
477
+ },
478
+ 'ci' => {
479
+ 'hide' => %w(ci/circleci ci/jenkins),
480
+ 'allow_failures' => %w(ci/circleci ci/travis),
481
+ },
482
+ )
483
+ assert_equal ['bar'], @spec.pull_request_required_statuses
484
+ end
485
+
486
+ test "revalidate_pull_requests_after defaults to `nil" do
487
+ @spec.expects(:load_config).returns({})
488
+ assert_nil @spec.revalidate_pull_requests_after
489
+ end
490
+
491
+ test "revalidate_pull_requests_after defaults to `nil` if `merge.timeout` cannot be parsed" do
492
+ @spec.expects(:load_config).returns(
493
+ 'merge' => {
494
+ 'revalidate_after' => 'ALSKhfjsdkf',
495
+ },
496
+ )
497
+ assert_nil @spec.revalidate_pull_requests_after
498
+ end
499
+
500
+ test "revalidate_after returns `merge.revalidate_after` if present" do
501
+ @spec.expects(:load_config).returns(
502
+ 'merge' => {
503
+ 'revalidate_after' => '5m30s',
504
+ },
505
+ )
506
+ assert_equal 330, @spec.revalidate_pull_requests_after.to_i
507
+ end
508
+
383
509
  test "#file is impacted by `machine.directory`" do
384
510
  subdir = '/foo/bar'
385
511
  @spec.stubs(:load_config).returns('machine' => {'directory' => subdir})
@@ -495,9 +621,9 @@ module Shipit
495
621
  assert_equal ['yarn install --no-progress'], @spec.dependencies_steps
496
622
  end
497
623
 
498
- test '#publish_yarn_package checks if version tag exists, and then invokes yarn publish script' do
624
+ test '#publish_yarn_package checks if version tag exists, and then invokes npm publish script' do
499
625
  @spec.stubs(:yarn?).returns(true).at_least_once
500
- assert_equal ['assert-npm-version-tag', 'yarn publish'], @spec.deploy_steps
626
+ assert_equal ['assert-npm-version-tag', 'npm publish'], @spec.deploy_steps
501
627
  end
502
628
 
503
629
  test 'yarn checklist takes precedence over npm checklist' do
@@ -225,6 +225,13 @@ module Shipit
225
225
  end
226
226
  end
227
227
 
228
+ test "transitioning to success schedule a MergePullRequests job" do
229
+ @deploy = shipit_deploys(:shipit_running)
230
+ assert_enqueued_with(job: MergePullRequestsJob, args: [@deploy.stack]) do
231
+ @deploy.complete!
232
+ end
233
+ end
234
+
228
235
  test "transitioning to success schedule a fetch of the deployed revision" do
229
236
  @deploy = shipit_deploys(:shipit_running)
230
237
  assert_enqueued_with(job: FetchDeployedRevisionJob, args: [@deploy.stack]) do
@@ -440,10 +447,9 @@ module Shipit
440
447
  private
441
448
 
442
449
  def expect_event(deploy)
443
- Pubsubstub::RedisPubSub.expects(:publish).at_least_once
444
- Pubsubstub::RedisPubSub.expects(:publish).with do |channel, event|
445
- data = JSON.load(event.data)
446
- channel == "stack.#{deploy.stack.id}" && data['url'] == "/#{deploy.stack.to_param}"
450
+ Pubsubstub.expects(:publish).at_least_once
451
+ Pubsubstub.expects(:publish).with do |channel, _payload, _options|
452
+ channel == "stack.#{deploy.stack.id}"
447
453
  end
448
454
  end
449
455
  end
@@ -0,0 +1,197 @@
1
+ require 'test_helper'
2
+
3
+ module Shipit
4
+ class PullRequestTest < ActiveSupport::TestCase
5
+ setup do
6
+ @stack = shipit_stacks(:shipit)
7
+ @pr = shipit_pull_requests(:shipit_pending)
8
+ @user = shipit_users(:walrus)
9
+ end
10
+
11
+ test ".request_merge! creates a record and schedule a refresh" do
12
+ pull_request = nil
13
+ assert_enqueued_with(job: RefreshPullRequestJob) do
14
+ pull_request = PullRequest.request_merge!(@stack, 64, @user)
15
+ end
16
+ assert_predicate pull_request, :persisted?
17
+ end
18
+
19
+ test ".request_merge! only track pull requests once" do
20
+ assert_difference -> { PullRequest.count }, +1 do
21
+ 5.times { PullRequest.request_merge!(@stack, 65, @user) }
22
+ end
23
+ end
24
+
25
+ test ".request_merge! retry canceled pull requests" do
26
+ original_merge_requested_at = @pr.merge_requested_at
27
+ @pr.cancel!
28
+ assert_predicate @pr, :canceled?
29
+ PullRequest.request_merge!(@stack, @pr.number, @user)
30
+ assert_predicate @pr.reload, :pending?
31
+ assert_not_equal original_merge_requested_at, @pr.merge_requested_at
32
+ assert_in_delta Time.now.utc, @pr.merge_requested_at, 1
33
+ end
34
+
35
+ test ".request_merge! retry rejected pull requests" do
36
+ original_merge_requested_at = @pr.merge_requested_at
37
+ @pr.reject!('merge_conflict')
38
+ assert_predicate @pr, :rejected?
39
+ PullRequest.request_merge!(@stack, @pr.number, @user)
40
+ assert_predicate @pr.reload, :pending?
41
+ assert_not_equal original_merge_requested_at, @pr.merge_requested_at
42
+ assert_in_delta Time.now.utc, @pr.merge_requested_at, 1
43
+ assert_nil @pr.rejection_reason
44
+ end
45
+
46
+ test ".request_merge! retry revalidating pull requests but keep the original request time" do
47
+ original_merge_requested_at = @pr.merge_requested_at
48
+ @pr.revalidate!
49
+ assert_predicate @pr, :revalidating?
50
+ PullRequest.request_merge!(@stack, @pr.number, @user)
51
+ assert_predicate @pr.reload, :pending?
52
+ assert_equal original_merge_requested_at, @pr.merge_requested_at
53
+ end
54
+
55
+ test ".extract_number can get a pull request number from different formats" do
56
+ assert_equal 42, PullRequest.extract_number(@stack, '42')
57
+ assert_equal 42, PullRequest.extract_number(@stack, '#42')
58
+ assert_equal 42, PullRequest.extract_number(@stack, 'https://github.com/Shopify/shipit-engine/pull/42')
59
+
60
+ assert_nil PullRequest.extract_number(@stack, 'https://github.com/ACME/shipit-engine/pull/42')
61
+
62
+ Shipit.expects(:github_domain).returns('github.acme.com').at_least_once
63
+ assert_equal 42, PullRequest.extract_number(@stack, 'https://github.acme.com/Shopify/shipit-engine/pull/42')
64
+ assert_nil PullRequest.extract_number(@stack, 'https://github.com/Shopify/shipit-engine/pull/42')
65
+ end
66
+
67
+ test "refresh! pulls state from GitHub" do
68
+ pull_request = shipit_pull_requests(:shipit_fetching)
69
+
70
+ head_sha = '64b3833d39def7ec65b57b42f496eb27ab4980b6'
71
+ Shipit.github_api.expects(:pull_request).with(@stack.github_repo_name, pull_request.number).returns(
72
+ stub(
73
+ id: 4_857_578,
74
+ url: 'https://api.github.com/repos/Shopify/shipit-engine/pulls/64',
75
+ title: 'Great feature',
76
+ state: 'open',
77
+ mergeable: true,
78
+ additions: 24,
79
+ deletions: 5,
80
+ head: stub(
81
+ ref: 'super-branch',
82
+ sha: head_sha,
83
+ ),
84
+ ),
85
+ )
86
+
87
+ author = stub(
88
+ id: 1234,
89
+ login: 'bob',
90
+ name: 'Bob the Builder',
91
+ email: 'bob@bob.com',
92
+ )
93
+ Shipit.github_api.expects(:commit).with(@stack.github_repo_name, head_sha).returns(
94
+ stub(
95
+ sha: head_sha,
96
+ author: author,
97
+ committer: author,
98
+ commit: stub(
99
+ message: 'Great feature',
100
+ author: stub(date: 1.day.ago),
101
+ committer: stub(date: 1.day.ago),
102
+ ),
103
+ stats: stub(
104
+ additions: 24,
105
+ deletions: 5,
106
+ ),
107
+ ),
108
+ )
109
+
110
+ Shipit.github_api.expects(:statuses).with(@stack.github_repo_name, head_sha).returns([stub(
111
+ state: 'success',
112
+ description: nil,
113
+ context: 'default',
114
+ target_url: 'http://example.com',
115
+ created_at: 1.day.ago,
116
+ )])
117
+
118
+ pull_request.refresh!
119
+
120
+ assert_predicate pull_request, :mergeable?
121
+ assert_predicate pull_request, :pending?
122
+ assert_equal 'super-branch', pull_request.branch
123
+
124
+ assert_not_nil pull_request.head
125
+ assert_predicate pull_request.head, :detached?
126
+ assert_predicate pull_request.head, :success?
127
+ end
128
+
129
+ test "#reject! records the reason" do
130
+ @pr.reject!('merge_conflict')
131
+ assert_equal 'merge_conflict', @pr.rejection_reason
132
+ end
133
+
134
+ test "transitionning from rejected to any other state clear the rejection reason" do
135
+ @pr.reject!('merge_conflict')
136
+ assert_equal 'merge_conflict', @pr.rejection_reason
137
+ @pr.retry!
138
+ assert_nil @pr.rejection_reason
139
+ assert_nil @pr.reload.rejection_reason
140
+ end
141
+
142
+ test "#reject_unless_mergeable! returns `false` if the PR is not yet mergeable" do
143
+ @pr.update!(mergeable: nil)
144
+ assert_predicate @pr, :not_mergeable_yet?
145
+ assert_equal false, @pr.reject_unless_mergeable!
146
+ assert_predicate @pr, :pending?
147
+ end
148
+
149
+ test "#reject_unless_mergeable! rejects the PR if it has a merge conflict" do
150
+ @pr.update!(mergeable: false)
151
+
152
+ assert_predicate @pr, :merge_conflict?
153
+ assert_equal true, @pr.reject_unless_mergeable!
154
+ assert_predicate @pr, :rejected?
155
+ assert_equal 'merge_conflict', @pr.rejection_reason
156
+ end
157
+
158
+ test "#reject_unless_mergeable! rejects the PR if it has a failing or pending CI status" do
159
+ @pr.head.statuses.create!(stack: @pr.stack, state: 'pending', context: 'ci/circle')
160
+
161
+ refute_predicate @pr, :all_status_checks_passed?
162
+ assert_equal true, @pr.reject_unless_mergeable!
163
+ assert_predicate @pr, :rejected?
164
+ assert_equal 'ci_failing', @pr.rejection_reason
165
+ end
166
+
167
+ test "#merge! revalidates the PR if it has been enqueued for too long" do
168
+ @pr.update!(revalidated_at: 5.hours.ago)
169
+
170
+ assert_predicate @pr, :need_revalidation?
171
+ assert_equal false, @pr.merge!
172
+ assert_predicate @pr, :revalidating?
173
+ end
174
+
175
+ test "#merge! raises a PullRequest::NotReady if the PR isn't mergeable yet" do
176
+ @pr.update!(mergeable: nil)
177
+
178
+ assert_predicate @pr, :not_mergeable_yet?
179
+ assert_raises PullRequest::NotReady do
180
+ @pr.merge!
181
+ end
182
+ @pr.reload
183
+ assert_predicate @pr, :pending?
184
+ end
185
+
186
+ test "status transitions emit hooks" do
187
+ job = assert_enqueued_with(job: EmitEventJob) do
188
+ @pr.reject!('merge_conflict')
189
+ end
190
+ params = job.arguments.first
191
+ assert_equal 'merge', params['event']
192
+ assert_json 'status', 'rejected', document: params['payload']
193
+ assert_json 'pull_request.rejection_reason', 'merge_conflict', document: params['payload']
194
+ assert_json 'pull_request.number', @pr.number, document: params['payload']
195
+ end
196
+ end
197
+ end