shipit-engine 0.15.0 → 0.16.0

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