shipit-engine 0.20.1 → 0.21.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 (109) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +43 -6
  3. data/app/assets/stylesheets/_base/_base.scss +4 -0
  4. data/app/assets/stylesheets/_pages/_commits.scss +3 -1
  5. data/app/assets/stylesheets/_pages/_deploy.scss +4 -2
  6. data/app/controllers/concerns/shipit/authentication.rb +1 -1
  7. data/app/controllers/shipit/api/base_controller.rb +6 -1
  8. data/app/controllers/shipit/api/pull_requests_controller.rb +1 -1
  9. data/app/controllers/shipit/commit_checks_controller.rb +1 -1
  10. data/app/controllers/shipit/shipit_controller.rb +1 -5
  11. data/app/controllers/shipit/stacks_controller.rb +2 -0
  12. data/app/controllers/shipit/tasks_controller.rb +1 -1
  13. data/app/controllers/shipit/webhooks_controller.rb +2 -2
  14. data/app/helpers/shipit/deploys_helper.rb +9 -0
  15. data/app/helpers/shipit/shipit_helper.rb +17 -15
  16. data/app/helpers/shipit/stacks_helper.rb +6 -1
  17. data/app/jobs/shipit/destroy_stack_job.rb +4 -2
  18. data/app/jobs/shipit/fetch_deployed_revision_job.rb +1 -1
  19. data/app/jobs/shipit/github_sync_job.rb +1 -1
  20. data/app/jobs/shipit/merge_pull_requests_job.rb +3 -3
  21. data/app/jobs/shipit/perform_task_job.rb +3 -0
  22. data/app/jobs/shipit/purge_old_deliveries_job.rb +1 -0
  23. data/app/models/shipit/api_client.rb +1 -1
  24. data/app/models/shipit/commit.rb +29 -6
  25. data/app/models/shipit/commit_deployment.rb +1 -1
  26. data/app/models/shipit/commit_deployment_status.rb +1 -1
  27. data/app/models/shipit/deploy_spec.rb +19 -2
  28. data/app/models/shipit/deploy_spec/bundler_discovery.rb +1 -10
  29. data/app/models/shipit/deploy_spec/file_system.rb +6 -0
  30. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +1 -1
  31. data/app/models/shipit/deploy_spec/lerna_discovery.rb +85 -0
  32. data/app/models/shipit/deploy_spec/npm_discovery.rb +103 -5
  33. data/app/models/shipit/deploy_spec/pypi_discovery.rb +4 -2
  34. data/app/models/shipit/deploy_spec/rubygems_discovery.rb +4 -2
  35. data/app/models/shipit/duration.rb +1 -1
  36. data/app/models/shipit/github_status.rb +1 -1
  37. data/app/models/shipit/hook.rb +4 -5
  38. data/app/models/shipit/output_chunk.rb +1 -1
  39. data/app/models/shipit/pull_request.rb +36 -15
  40. data/app/models/shipit/stack.rb +15 -9
  41. data/app/models/shipit/status/common.rb +4 -0
  42. data/app/models/shipit/status/group.rb +4 -0
  43. data/app/models/shipit/task.rb +20 -8
  44. data/app/models/shipit/task_definition.rb +2 -2
  45. data/app/models/shipit/undeployed_commit.rb +13 -2
  46. data/app/models/shipit/user.rb +1 -1
  47. data/app/serializers/shipit/pull_request_serializer.rb +1 -1
  48. data/app/serializers/shipit/tail_task_serializer.rb +1 -1
  49. data/app/views/shipit/ccmenu/project.xml.builder +9 -8
  50. data/app/views/shipit/deploys/_deploy.html.erb +3 -2
  51. data/app/views/shipit/stacks/_banners.html.erb +4 -1
  52. data/app/views/shipit/stacks/settings.html.erb +4 -0
  53. data/app/views/shipit/statuses/_group.html.erb +1 -1
  54. data/app/views/shipit/statuses/_status.html.erb +1 -1
  55. data/app/views/shipit/tasks/_task.html.erb +1 -2
  56. data/config/locales/en.yml +2 -0
  57. data/config/secrets.development.example.yml +0 -4
  58. data/config/secrets.development.shopify.yml +1 -5
  59. data/db/migrate/20170904103242_reindex_deliveries.rb +7 -0
  60. data/db/migrate/20171120161420_add_base_info_to_pull_request.rb +7 -0
  61. data/db/migrate/20180202220850_add_aborted_by_to_tasks.rb +5 -0
  62. data/lib/shipit.rb +15 -23
  63. data/lib/shipit/command.rb +11 -3
  64. data/lib/shipit/engine.rb +0 -4
  65. data/lib/shipit/stack_commands.rb +3 -1
  66. data/lib/shipit/version.rb +1 -1
  67. data/lib/snippets/assert-lerna-fixed-version-tag +21 -0
  68. data/lib/snippets/assert-lerna-independent-version-tags +28 -0
  69. data/lib/snippets/generate-local-npmrc +19 -0
  70. data/lib/snippets/misconfigured-npm-publish-config +8 -0
  71. data/lib/snippets/publish-lerna-independent-packages +39 -0
  72. data/lib/snippets/push-to-heroku +5 -5
  73. data/lib/tasks/cron.rake +1 -1
  74. data/lib/tasks/dev.rake +1 -1
  75. data/test/controllers/api/deploys_controller_test.rb +19 -0
  76. data/test/controllers/api/stacks_controller_test.rb +1 -1
  77. data/test/controllers/github_authentication_controller_test.rb +1 -1
  78. data/test/controllers/stacks_controller_test.rb +10 -0
  79. data/test/controllers/tasks_controller_test.rb +2 -0
  80. data/test/controllers/webhooks_controller_test.rb +0 -7
  81. data/test/dummy/config/secrets.yml +0 -2
  82. data/test/dummy/db/development.sqlite3 +0 -0
  83. data/test/dummy/db/schema.rb +5 -3
  84. data/test/dummy/db/test.sqlite3 +0 -0
  85. data/test/fixtures/shipit/commits.yml +53 -0
  86. data/test/fixtures/shipit/pull_requests.yml +52 -0
  87. data/test/fixtures/shipit/stacks.yml +35 -0
  88. data/test/fixtures/shipit/statuses.yml +27 -0
  89. data/test/fixtures/shipit/tasks.yml +14 -0
  90. data/test/helpers/queries_helper.rb +1 -1
  91. data/test/jobs/merge_pull_requests_job_test.rb +19 -2
  92. data/test/jobs/perform_task_job_test.rb +26 -2
  93. data/test/models/commits_test.rb +55 -6
  94. data/test/models/deploy_spec_test.rb +288 -52
  95. data/test/models/deploys_test.rb +7 -7
  96. data/test/models/hook_test.rb +4 -3
  97. data/test/models/pull_request_test.rb +78 -24
  98. data/test/models/stacks_test.rb +21 -17
  99. data/test/models/status/group_test.rb +6 -0
  100. data/test/models/undeployed_commits_test.rb +9 -0
  101. data/test/models/users_test.rb +2 -2
  102. data/test/test_helper.rb +1 -1
  103. metadata +211 -222
  104. data/app/assets/javascripts/shipit_bs.js.coffee +0 -2
  105. data/app/assets/stylesheets/shipit_bs.scss +0 -22
  106. data/app/views/bootstrap/shipit/missing_settings.html.erb +0 -97
  107. data/app/views/bootstrap/shipit/stacks/new.html.erb +0 -44
  108. data/app/views/layouts/shipit_bootstrap.html.erb +0 -44
  109. data/lib/shipit/template_renderer_extension.rb +0 -16
@@ -87,3 +87,55 @@ shipit_pending_merged:
87
87
  mergeable: null
88
88
  additions: 23
89
89
  deletions: 43
90
+
91
+ shipit_mergeable_pending_ci:
92
+ stack: shipit
93
+ number: 67
94
+ title: Super duper nice feature
95
+ merge_status: pending
96
+ merge_requested_at: <%= 5.minute.ago.to_s(:db) %>
97
+ revalidated_at: <%= 5.minute.ago.to_s(:db) %>
98
+ merge_requested_by: walrus
99
+ github_id: 424244242424241
100
+ api_url: https://api.github.com/repos/shopify/shipit-engine/pulls/67
101
+ state: open
102
+ branch: feature-67
103
+ head_id: 4
104
+ mergeable: true
105
+ additions: 23
106
+ deletions: 43
107
+
108
+ shipit_pending_expired:
109
+ stack: shipit
110
+ number: 68
111
+ title: Noice !
112
+ merge_status: pending
113
+ merge_requested_at: <%= 50.minute.ago.to_s(:db) %>
114
+ revalidated_at: <%= 25.minute.ago.to_s(:db) %>
115
+ merge_requested_by: walrus
116
+ github_id: 48484848484848
117
+ api_url: https://api.github.com/repos/shopify/shipit-engine/pulls/68
118
+ state: open
119
+ branch: feature-68
120
+ head_id: 8
121
+ mergeable: true
122
+ additions: 23
123
+ deletions: 43
124
+
125
+ cyclimse_pending_merged:
126
+ id: 99
127
+ stack: cyclimse
128
+ number: 66
129
+ merge_status: merged
130
+ merge_requested_by: walrus
131
+ merge_requested_at: <%= 3.minute.ago.to_s(:db) %>
132
+ merged_at: <%= 2.minute.ago.to_s(:db) %>
133
+ revalidated_at: <%= 3.minute.ago.to_s(:db) %>
134
+ github_id: 43243243243232
135
+ api_url: https://api.github.com/repos/shopify/cyclimse/pulls/66
136
+ state: closed
137
+ branch: feature-64
138
+ head_id: 8
139
+ mergeable: null
140
+ additions: 23
141
+ deletions: 43
@@ -123,3 +123,38 @@ undeployed_stack:
123
123
  }
124
124
  }
125
125
  updated_at: <%= 8.days.ago.to_s(:db) %>
126
+
127
+ soc:
128
+ repo_owner: "shopify"
129
+ repo_name: "soc"
130
+ environment: "production"
131
+ branch: master
132
+ tasks_count: 0
133
+ undeployed_commits_count: 2
134
+ cached_deploy_spec: >
135
+ {
136
+ "machine": {"environment": {}},
137
+ "review": {
138
+ "checklist": ["foo", "bar", "baz"],
139
+ "monitoring": [
140
+ {"image": "https://example.com/monitor.png", "width": 200, "height": 300}
141
+ ]
142
+ },
143
+ "dependencies": {"override": []},
144
+ "deploy": {"override": null},
145
+ "rollback": {"override": ["echo 'Rollback!'"]},
146
+ "fetch": ["echo '42'"],
147
+ "tasks": {
148
+ "restart": {
149
+ "action": "Restart application",
150
+ "description": "Restart app and job servers",
151
+ "steps": [
152
+ "cap $ENVIRONMENT deploy:restart"
153
+ ]
154
+ }
155
+ },
156
+ "ci": {
157
+ "blocking": ["soc/compliance"]
158
+ }
159
+ }
160
+ updated_at: <%= 8.days.ago.to_s(:db) %>
@@ -98,3 +98,30 @@ shipit_pending_pr_success_travis:
98
98
  created_at: <%= 9.days.ago.to_s(:db) %>
99
99
  state: success
100
100
  target_url: "http://www.example.com"
101
+
102
+ soc_first:
103
+ stack: soc
104
+ commit_id: 101 # soc_first
105
+ description: Woops
106
+ context: soc/compliance
107
+ created_at: <%= 9.days.ago.to_s(:db) %>
108
+ state: failure
109
+ target_url: "http://www.example.com"
110
+
111
+ soc_second:
112
+ stack: soc
113
+ commit_id: 102 # soc_second
114
+ description: All good
115
+ context: soc/compliance
116
+ created_at: <%= 7.days.ago.to_s(:db) %>
117
+ state: success
118
+ target_url: "http://www.example.com"
119
+
120
+ soc_third:
121
+ stack: soc
122
+ commit_id: 103 # soc_third
123
+ description: All good
124
+ context: soc/compliance
125
+ created_at: <%= 7.days.ago.to_s(:db) %>
126
+ state: success
127
+ target_url: "http://www.example.com"
@@ -120,3 +120,17 @@ shipit_rollback:
120
120
  created_at: <%= (60 - 8).minutes.ago.to_s(:db) %>
121
121
  started_at: <%= (60 - 8).minutes.ago.to_s(:db) %>
122
122
  ended_at: <%= (60 - 7).minutes.ago.to_s(:db) %>
123
+
124
+ soc_deploy:
125
+ id: 9
126
+ user: walrus
127
+ since_commit_id: 101 # soc_first
128
+ until_commit_id: 101 # soc_first
129
+ type: Shipit::Deploy
130
+ stack: soc
131
+ status: success
132
+ additions: 1
133
+ deletions: 1
134
+ created_at: <%= (60 - 1).minutes.ago.to_s(:db) %>
135
+ started_at: <%= (60 - 1).minutes.ago.to_s(:db) %>
136
+ ended_at: <%= (60 - 3).minutes.ago.to_s(:db) %>
@@ -50,7 +50,7 @@ module QueriesHelper
50
50
 
51
51
  # FIXME: this seems bad. we should probably have a better way to indicate
52
52
  # the query was cached
53
- return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
53
+ return if values[:name] == 'CACHE' || ignore.any? { |x| x =~ sql }
54
54
  log << sql
55
55
  end
56
56
  end
@@ -11,6 +11,8 @@ module Shipit
11
11
  @not_ready_pr = shipit_pull_requests(:shipit_pending_not_mergeable_yet)
12
12
  @closed_pr = shipit_pull_requests(:shipit_pending_closed)
13
13
  @merged_pr = shipit_pull_requests(:shipit_pending_merged)
14
+ @expired_pr = shipit_pull_requests(:shipit_pending_expired)
15
+ @mergable_pending_ci = shipit_pull_requests(:shipit_mergeable_pending_ci)
14
16
  end
15
17
 
16
18
  test "#perform rejects unmergeable PRs and merge the others" do
@@ -50,6 +52,13 @@ module Shipit
50
52
  assert_predicate @pending_pr.reload, :pending?
51
53
  end
52
54
 
55
+ test "#perform revalidate PRs but do not attempt to merge any if the stack doesn't allow merges" do
56
+ PullRequest.any_instance.stubs(:refresh!)
57
+ @stack.update!(lock_reason: 'Maintenance')
58
+ @job.perform(@stack)
59
+ assert_predicate @expired_pr.reload, :revalidating?
60
+ end
61
+
53
62
  test "#perform schedules a new job if the first PR in the queue is not mergeable yet" do
54
63
  PullRequest.any_instance.stubs(:refresh!)
55
64
 
@@ -67,11 +76,19 @@ module Shipit
67
76
  assert_predicate @closed_pr.reload, :canceled?
68
77
  end
69
78
 
70
- test "#perform completes merge requests for already merged PRs" do
79
+ test "#perform cancels merge requests for manually merged PRs" do
80
+ @pending_pr.cancel!
81
+ PullRequest.any_instance.stubs(:refresh!)
82
+ @job.perform(@stack)
83
+ assert_predicate @merged_pr.reload, :canceled?
84
+ end
85
+
86
+ test "#perform does not reject pull requests with pending statuses" do
71
87
  @pending_pr.cancel!
72
88
  PullRequest.any_instance.stubs(:refresh!)
73
89
  @job.perform(@stack)
74
- assert_predicate @merged_pr.reload, :merged?
90
+ refute_predicate @mergable_pending_ci.reload, :rejected?
91
+ refute_predicate @mergable_pending_ci.reload, :merged?
75
92
  end
76
93
  end
77
94
  end
@@ -67,15 +67,39 @@ module Shipit
67
67
  @job.perform(@deploy)
68
68
  end
69
69
 
70
- test "mark deploy as error if a command timeout" do
71
- Command.any_instance.expects(:stream!).at_least_once.raises(Command::TimedOut)
70
+ test "mark deploy as error an unexpected exception is raised" do
71
+ Command.any_instance.expects(:stream!).at_least_once.raises(Command::Denied)
72
72
 
73
73
  @job.perform(@deploy)
74
74
 
75
75
  assert_equal 'failed', @deploy.reload.status
76
+ assert_includes @deploy.chunk_output, 'Denied'
77
+ end
78
+
79
+ test "mark deploy as timedout if a command timeout" do
80
+ Command.any_instance.expects(:stream!).at_least_once.raises(Command::TimedOut)
81
+
82
+ @job.perform(@deploy)
83
+
84
+ assert_equal 'timedout', @deploy.reload.status
76
85
  assert_includes @deploy.chunk_output, 'TimedOut'
77
86
  end
78
87
 
88
+ test "mark deploy as timedout if a command exit in one of the codes in Shipit.timeout_exit_codes" do
89
+ previous_exit_codes = Shipit.timeout_exit_codes
90
+ begin
91
+ Shipit.timeout_exit_codes = [70].freeze
92
+
93
+ Command.any_instance.expects(:stream!).at_least_once.raises(Command::Failed.new('Blah', 70))
94
+
95
+ @job.perform(@deploy)
96
+
97
+ assert_equal 'timedout', @deploy.reload.status
98
+ ensure
99
+ Shipit.timeout_exit_codes = previous_exit_codes
100
+ end
101
+ end
102
+
79
103
  test "records stack support for rollbacks and fetching deployed revision" do
80
104
  @job.stubs(:capture!)
81
105
  @commands = stub(:commands)
@@ -148,8 +148,8 @@ module Shipit
148
148
  author: walrus,
149
149
  committer: walrus,
150
150
  sha: "ab12",
151
- authored_at: DateTime.now,
152
- committed_at: DateTime.now,
151
+ authored_at: Time.now,
152
+ committed_at: Time.now,
153
153
  message: "more fish!",
154
154
  )
155
155
  end
@@ -176,8 +176,8 @@ module Shipit
176
176
  author: walrus,
177
177
  committer: walrus,
178
178
  sha: "ab12",
179
- authored_at: DateTime.now,
180
- committed_at: DateTime.now,
179
+ authored_at: Time.now,
180
+ committed_at: Time.now,
181
181
  message: "more fish!",
182
182
  )
183
183
  @stack.reload
@@ -217,8 +217,8 @@ module Shipit
217
217
  author: walrus,
218
218
  committer: walrus,
219
219
  sha: "ab12",
220
- authored_at: DateTime.now,
221
- committed_at: DateTime.now,
220
+ authored_at: Time.now,
221
+ committed_at: Time.now,
222
222
  message: "more fish!",
223
223
  )
224
224
  stack.reload
@@ -306,6 +306,37 @@ module Shipit
306
306
  refute_predicate commit, :deployable?
307
307
  end
308
308
 
309
+ test "#deployable? is false if a blocking status is missing on a previous undeployed commit" do
310
+ blocking_commit = shipit_commits(:soc_second)
311
+ blocking_commit.statuses.delete_all
312
+
313
+ assert_predicate blocking_commit, :pending?
314
+ assert_predicate blocking_commit, :blocking?
315
+
316
+ commit = shipit_commits(:soc_third)
317
+ refute_predicate commit, :deployable?
318
+ end
319
+
320
+ test "#deployable? is false if a blocking status is failing on a previous undeployed commit" do
321
+ blocking_commit = shipit_commits(:soc_second)
322
+ blocking_commit.statuses.update_all(state: 'failure')
323
+
324
+ assert_predicate blocking_commit, :failure?
325
+ assert_predicate blocking_commit, :blocking?
326
+
327
+ commit = shipit_commits(:soc_third)
328
+ refute_predicate commit, :deployable?
329
+ end
330
+
331
+ test "#deployable? is true if no blocking status is failing or missing on a previous undeployed commit" do
332
+ blocking_commit = shipit_commits(:soc_second)
333
+ assert_predicate blocking_commit, :success?
334
+ refute_predicate blocking_commit, :blocking?
335
+
336
+ commit = shipit_commits(:soc_third)
337
+ assert_predicate commit, :deployable?
338
+ end
339
+
309
340
  expected_webhook_transitions = { # we expect deployable_status to fire on these transitions, and not on any others
310
341
  'unknown' => %w(pending success failure error),
311
342
  'pending' => %w(success failure error),
@@ -551,6 +582,24 @@ module Shipit
551
582
  assert revert.revert_of?(commit)
552
583
  end
553
584
 
585
+ test "deploy_requested_at defaults to commit created_at" do
586
+ assert_equal @commit.deploy_requested_at, @commit.created_at
587
+ end
588
+
589
+ test "when merged via the queue, deploy_requested_at is merge_requested_at" do
590
+ commit = shipit_commits(:cyclimse_merged)
591
+ assert_predicate commit, :pull_request?
592
+ assert_equal commit.pull_request, shipit_pull_requests(:cyclimse_pending_merged)
593
+ assert_equal commit.deploy_requested_at, commit.pull_request.merge_requested_at
594
+ end
595
+
596
+ test "when merged manually after being queued, deploy_requested_at is created_at" do
597
+ pr = shipit_pull_requests(:cyclimse_pending_merged)
598
+ pr.cancel!
599
+ commit = shipit_commits(:cyclimse_merged)
600
+ assert_equal commit.deploy_requested_at, commit.created_at
601
+ end
602
+
554
603
  private
555
604
 
556
605
  def expect_event(stack)
@@ -284,11 +284,16 @@ module Shipit
284
284
  'require' => [],
285
285
  'ignore' => [],
286
286
  'revalidate_after' => nil,
287
+ 'max_divergence' => {
288
+ 'commits' => nil,
289
+ 'age' => nil,
290
+ },
287
291
  },
288
292
  'ci' => {
289
293
  'hide' => [],
290
294
  'allow_failures' => [],
291
295
  'require' => [],
296
+ 'blocking' => [],
292
297
  },
293
298
  'machine' => {'environment' => {}, 'directory' => nil, 'cleanup' => true},
294
299
  'review' => {'checklist' => [], 'monitoring' => [], 'checks' => []},
@@ -318,59 +323,19 @@ module Shipit
318
323
  assert_equal 'SAFETY_DISABLED', variable_definition.name
319
324
  end
320
325
 
321
- test "task definitions prepend bundle exec by default" do
322
- @spec.expects(:load_config).returns('tasks' => {'restart' => {'steps' => %w(foo)}})
323
- @spec.expects(:bundler?).returns(true).at_least_once
324
- assert_deprecated(/Automatically prepending `bundle exec`/) do
325
- definition = @spec.find_task_definition('restart')
326
- assert_equal ['bundle exec foo'], definition.steps
327
- end
328
- end
329
-
330
- test "task definitions prepend bundle exec if enabled" do
331
- Shipit.expects(:automatically_prepend_bundle_exec).returns(true).at_least_once
326
+ test "task definitions don't prepend bundle exec by default" do
332
327
  @spec.expects(:load_config).returns('tasks' => {'restart' => {'steps' => %w(foo)}})
333
- @spec.expects(:bundler?).returns(true).at_least_once
334
328
  definition = @spec.find_task_definition('restart')
335
-
336
- assert_equal ['bundle exec foo'], definition.steps
337
- end
338
-
339
- test "task definitions do not prepend bundle exec if disabled" do
340
- Shipit.expects(:automatically_prepend_bundle_exec).returns(false).at_least_once
341
- @spec.expects(:load_config).returns('tasks' => {'restart' => {'steps' => %w(foo)}})
342
- definition = @spec.find_task_definition('restart')
343
-
344
329
  assert_equal ['foo'], definition.steps
345
330
  end
346
331
 
347
- test "task definitions do not prepend bundle exec if the task already does" do
348
- Shipit.expects(:automatically_prepend_bundle_exec).returns(true).at_least_once
349
- @spec.expects(:load_config).returns('tasks' => {'restart' => {'steps' => ['bundle exec foo']}})
350
- @spec.stubs(:bundler?).returns(true)
351
- definition = @spec.find_task_definition('restart')
352
-
353
- assert_equal ['bundle exec foo'], definition.steps
354
- end
355
-
356
- test "task definitions do not prepend bundle exec if depedency step is overridden" do
357
- @spec.expects(:load_config).returns(
358
- 'dependencies' => {'override' => []},
359
- 'tasks' => {'restart' => {'steps' => %w(foo)}},
360
- )
361
- @spec.expects(:bundler?).returns(true).at_least_once
362
- definition = @spec.find_task_definition('restart')
363
-
364
- assert_equal ['foo'], definition.steps
365
- end
366
-
367
- test "task definitions prepend bundle exec before serialization" do
332
+ test "task definitions don't bundle exec before serialization" do
368
333
  @spec.expects(:discover_task_definitions).returns('restart' => {'steps' => %w(foo)})
369
334
  @spec.expects(:bundler?).returns(true).at_least_once
370
335
 
371
336
  cached_spec = DeploySpec.load(DeploySpec.dump(@spec))
372
337
  definition = cached_spec.find_task_definition('restart')
373
- assert_equal ['bundle exec foo'], definition.steps
338
+ assert_equal ['foo'], definition.steps
374
339
  end
375
340
 
376
341
  test "#task_definitions returns kubernetes commands as well as comands from the config" do
@@ -455,6 +420,16 @@ module Shipit
455
420
  assert_equal %w(ci/circleci ci/jenkins), @spec.hidden_statuses
456
421
  end
457
422
 
423
+ test "#required_statuses automatically includes #blocking_statuses" do
424
+ @spec.expects(:load_config).returns(
425
+ 'ci' => {
426
+ 'require' => %w(ci/circleci),
427
+ 'blocking' => %w(soc/compliance),
428
+ },
429
+ )
430
+ assert_equal %w(ci/circleci soc/compliance), @spec.required_statuses
431
+ end
432
+
458
433
  test "pull_request_ignored_statuses defaults to the union of ci.hide and ci.allow_failures" do
459
434
  @spec.expects(:load_config).returns(
460
435
  'ci' => {
@@ -573,26 +548,43 @@ module Shipit
573
548
  assert_equal ['fake gem task'], @spec.deploy_steps
574
549
  end
575
550
 
551
+ test 'lerna monorepos take priority over solo npm deploys' do
552
+ @spec.expects(:discover_npm_package).never
553
+ @spec.stubs(:discover_lerna_packages).returns(['fake monorepo task']).once
554
+
555
+ assert_equal ['fake monorepo task'], @spec.deploy_steps
556
+ end
557
+
558
+ test '#lerna? is false if there is no lerna.json' do
559
+ @spec.expects(:lerna_json).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}"))
560
+ refute @spec.lerna?
561
+ end
562
+
576
563
  test '#npm? is false if there is no package.json' do
577
564
  @spec.expects(:package_json).returns(Shipit::Engine.root.join("tmp-#{SecureRandom.hex}"))
578
565
  refute @spec.npm?
579
566
  end
580
567
 
581
- test '#npm? is false if npm package is private' do
568
+ test '#npm? is true if npm package is public' do
582
569
  file = Pathname.new('/tmp/fake_package.json')
583
- file.write('{"private": true}')
570
+ file.write('{"private": false}')
584
571
 
585
572
  @spec.expects(:package_json).returns(file)
586
- refute @spec.npm?
573
+
574
+ assert @spec.npm?
587
575
  end
588
576
 
589
- test '#npm? is true if npm package is public' do
577
+ test '#npm? is false if npm package is private' do
590
578
  file = Pathname.new('/tmp/fake_package.json')
591
- file.write('{"private": false}')
579
+ file.write('{"private": true}')
592
580
 
593
581
  @spec.expects(:package_json).returns(file)
582
+ refute @spec.npm?
583
+ end
594
584
 
595
- assert @spec.npm?
585
+ test 'lerna monorepos have a checklist' do
586
+ @spec.stubs(:lerna?).returns(true).at_least_once
587
+ assert_match(/lerna publish --skip-npm/, @spec.review_checklist[0])
596
588
  end
597
589
 
598
590
  test 'npm packages have a checklist' do
@@ -600,14 +592,223 @@ module Shipit
600
592
  assert_match(/npm version/, @spec.review_checklist[0])
601
593
  end
602
594
 
595
+ test '#lerna_version returns the monorepo root version number' do
596
+ file = Pathname.new('/tmp/fake_lerna.json')
597
+ file.write('{"version": "1.0.0-beta.1"}')
598
+
599
+ @spec.expects(:lerna_json).returns(file)
600
+ assert_equal '1.0.0-beta.1', @spec.lerna_version
601
+ end
602
+
603
+ test '#package_version returns the version number' do
604
+ file = Pathname.new('/tmp/fake_package.json')
605
+ file.write('{"version": "1.0.0-beta.1"}')
606
+
607
+ @spec.expects(:package_json).returns(file)
608
+ assert_equal '1.0.0-beta.1', @spec.package_version
609
+ end
610
+
611
+ test '#dist_tag returns "latest" if the version does not contains a standard pre-release tag' do
612
+ assert_equal 'latest', @spec.dist_tag('1.0.0')
613
+ assert_equal 'latest', @spec.dist_tag('1.0.0-shopifyv4')
614
+ end
615
+
616
+ test '#dist_tag returns "next" if the version contains a pre-release tag' do
617
+ assert_equal 'next', @spec.dist_tag('1.0.0-alpha.1')
618
+ assert_equal 'next', @spec.dist_tag('1.0.0-beta')
619
+ assert_equal 'next', @spec.dist_tag('1.0.0-rc.3')
620
+ assert_equal 'next', @spec.dist_tag('1.0.0-next')
621
+ end
622
+
623
+ test '#dependencies_steps returns lerna setup if a `lerna.json` is present' do
624
+ @spec.expects(:lerna?).returns(true).at_least_once
625
+ assert_equal ['npm install --no-progress', 'node_modules/.bin/lerna bootstrap'], @spec.dependencies_steps
626
+ end
627
+
603
628
  test '#dependencies_steps returns `npm install` if a `package.json` is present' do
604
629
  @spec.expects(:npm?).returns(true).at_least_once
605
630
  assert_equal ['npm install --no-progress'], @spec.dependencies_steps
606
631
  end
607
632
 
633
+ test '#publish_lerna_packages checks if independent version tags exist, and then invokes lerna deploy script' do
634
+ @spec.stubs(:lerna?).returns(true)
635
+ @spec.stubs(:lerna_version).returns('independent')
636
+ assert_equal 'assert-lerna-independent-version-tags', @spec.deploy_steps[0]
637
+ assert_equal 'publish-lerna-independent-packages', @spec.deploy_steps[1]
638
+ end
639
+
640
+ test '#publish_lerna_packages checks if fixed version tag exists, and then invokes lerna deploy script' do
641
+ @spec.stubs(:lerna?).returns(true)
642
+ @spec.stubs(:lerna_version).returns('1.0.0')
643
+ assert_equal 'assert-lerna-fixed-version-tag', @spec.deploy_steps[0]
644
+ assert_equal 'node_modules/.bin/lerna publish --yes --skip-git --repo-version 1.0.0 --force-publish=* --npm-tag latest', @spec.deploy_steps[1]
645
+ end
646
+
647
+ test '#enforce_publish_config? is false when Shipit.enforce_publish_config is nil' do
648
+ Shipit.stubs(:enforce_publish_config).returns(nil)
649
+ refute @spec.enforce_publish_config?
650
+ end
651
+
652
+ test '#enforce_publish_config? is false when Shipit.enforce_publish_config is 0' do
653
+ Shipit.stubs(:enforce_publish_config).returns('0')
654
+ refute @spec.enforce_publish_config?
655
+ end
656
+
657
+ test '#enforce_publish_config? is true when Shipit.enforce_publish_config is 1' do
658
+ Shipit.stubs(:enforce_publish_config).returns('1')
659
+ assert @spec.enforce_publish_config?
660
+ end
661
+
662
+ test '#valid_publish_config? is false when enforce_publish_config? is true and publishConfig is missing from package.json' do
663
+ Shipit.stubs(:private_npm_registry).returns('some_private_registry')
664
+ @spec.stubs(:enforce_publish_config?).returns(true)
665
+ @spec.stubs(:publish_config_access).returns('restricted')
666
+
667
+ package_json = Pathname.new('/tmp/fake_package.json')
668
+ package_json.write('{"name": "foo"}')
669
+
670
+ @spec.expects(:package_json).returns(package_json)
671
+ refute @spec.valid_publish_config?
672
+ end
673
+
674
+ test '#valid_publish_config? is true when enforce_publish_config? is true and publishConfig.access is public' do
675
+ Shipit.stubs(:private_npm_registry).returns('some_private_registry')
676
+ @spec.stubs(:enforce_publish_config?).returns(true)
677
+ @spec.stubs(:publish_config_access).returns('public')
678
+ @spec.stubs(:publish_config).returns('something')
679
+
680
+ assert @spec.valid_publish_config?
681
+ end
682
+
683
+ test '#valid_publish_config? is true when shipit does not enforce a publishConfig' do
684
+ @spec.stubs(:lerna_version).returns('1.0.0')
685
+ assert @spec.valid_publish_config?
686
+ end
687
+
688
+ test '#publish_config returns publishConfig from package.json' do
689
+ package_json = Pathname.new('/tmp/fake_package.json')
690
+ package_json.write('{"publishConfig": "foo"}')
691
+
692
+ @spec.expects(:package_json).returns(package_json)
693
+ assert_equal "foo", @spec.publish_config
694
+ end
695
+
696
+ test '#valid_publish_config_access? is false when publishConfig.access is invalid' do
697
+ @spec.stubs(:publish_config_access).returns('foo')
698
+ refute @spec.valid_publish_config_access?
699
+ end
700
+
701
+ test '#valid_publish_config_access? is true when publishConfig.access is public or restricted' do
702
+ @spec.stubs(:publish_config_access).returns('public')
703
+ assert @spec.valid_publish_config_access?
704
+
705
+ @spec.stubs(:publish_config_access).returns('restricted')
706
+ assert @spec.valid_publish_config_access?
707
+ end
708
+
709
+ test '#publish_config_access is restricted when enforce_publish_config? is true and publishConfig is missing' do
710
+ package_json = Pathname.new('/tmp/fake_package.json')
711
+ package_json.write('{"name": "@shopify/foo"}')
712
+
713
+ @spec.stubs(:enforce_publish_config?).returns(true)
714
+ @spec.expects(:package_json).returns(package_json)
715
+ assert_equal 'restricted', @spec.publish_config_access
716
+ end
717
+
718
+ test '#publish_config_access is public when enforce_publish_config? is false and publishConfig is missing' do
719
+ package_json = Pathname.new('/tmp/fake_package.json')
720
+ package_json.write('{"name": "@shopify/foo"}')
721
+
722
+ @spec.stubs(:enforce_publish_config?).returns(false)
723
+ @spec.expects(:package_json).returns(package_json)
724
+ assert_equal 'public', @spec.publish_config_access
725
+ end
726
+
727
+ test '#publish_config_access returns publishConfig.access from package.json when enforce_publish_config? is true' do
728
+ package_json = Pathname.new('/tmp/fake_package.json')
729
+ package_json.write('{
730
+ "name": "@shopify/foo",
731
+ "publishConfig": {
732
+ "access": "foo"
733
+ }
734
+ }')
735
+
736
+ @spec.stubs(:enforce_publish_config?).returns(false)
737
+ @spec.expects(:package_json).returns(package_json)
738
+ assert_equal 'foo', @spec.publish_config_access
739
+ end
740
+
741
+ test "#scoped_package? is false when Shipit.npm_org_scope is not set and the package is private" do
742
+ Shipit.stubs(:npm_org_scope).returns(nil)
743
+ @spec.stubs(:publish_config_access).returns('restricted')
744
+ refute @spec.scoped_package?
745
+ end
746
+
747
+ test "#scoped_package? is true when Shipit.npm_org_scope is set and package_name starts with scope and the package is private" do
748
+ Shipit.stubs(:npm_org_scope).returns('@shopify')
749
+ @spec.stubs(:publish_config_access).returns('restricted')
750
+ @spec.stubs(:package_name).returns('@shopify/polaris')
751
+ assert @spec.scoped_package?
752
+ end
753
+
754
+ test "#private_scoped_package? is false when private packages are not scoped" do
755
+ @spec.stubs(:scoped_package?).returns(false)
756
+ @spec.stubs(:publish_config_access).returns("restricted")
757
+ refute @spec.private_scoped_package?
758
+ end
759
+
760
+ test "#private_scoped_package? is true when private packages are scoped" do
761
+ @spec.stubs(:scoped_package?).returns(true)
762
+ @spec.stubs(:publish_config_access).returns("restricted")
763
+ assert @spec.private_scoped_package?
764
+ end
765
+
608
766
  test '#publish_npm_package checks if version tag exists, and then invokes npm deploy script' do
609
767
  @spec.stubs(:npm?).returns(true)
610
- assert_equal ['assert-npm-version-tag', 'npm publish'], @spec.deploy_steps
768
+ @spec.stubs(:package_version).returns('1.0.0')
769
+ @spec.stubs(:valid_publish_config?).returns(true)
770
+ @spec.stubs(:publish_config_access).returns('restricted')
771
+ @spec.stubs(:registry).returns("@private:registry=some_private_registry")
772
+ assert_equal ['assert-npm-version-tag', 'npm publish --tag latest --access restricted'], @spec.deploy_steps
773
+ end
774
+
775
+ test '#npmrc_contents returns a scoped private package configuration when the package is scoped and private' do
776
+ registry = "@shopify:registry=some_private_registry"
777
+ Shipit.stubs(:npm_org_scope).returns('@shopify')
778
+ Shipit.stubs(:private_npm_registry).returns('some_private_registry')
779
+ @spec.stubs(:scoped_package?).returns(true)
780
+ @spec.stubs(:publish_config_access).returns('restricted')
781
+ assert_equal registry, @spec.registry
782
+ end
783
+
784
+ test '#npmrc_contents returns a public scoped package configuration when the package is scoped and public' do
785
+ registry = "@shopify:registry=https://registry.npmjs.org/"
786
+ Shipit.stubs(:npm_org_scope).returns('@shopify')
787
+ @spec.stubs(:scoped_package?).returns(true)
788
+ @spec.stubs(:publish_config_access).returns('public')
789
+ assert_equal registry, @spec.registry
790
+ end
791
+
792
+ test '#npmrc_contents returns a public non-scoped package configuration when the package is not scoped and public' do
793
+ registry = "registry=https://registry.npmjs.org/"
794
+ @spec.stubs(:scoped_package?).returns(false)
795
+ @spec.stubs(:publish_config_access).returns('public')
796
+ assert_equal registry, @spec.registry
797
+ end
798
+
799
+ test '#publish_lerna_packages guesses npm tag' do
800
+ @spec.stubs(:lerna?).returns(true)
801
+ @spec.stubs(:lerna_version).returns('1.0.0-alpha.1')
802
+ assert_match(/--npm-tag next/, @spec.deploy_steps.last)
803
+ end
804
+
805
+ test '#publish_npm_package checks if version tag and a pre-release flag exist, and then invokes npm deploy script' do
806
+ @spec.stubs(:npm?).returns(true)
807
+ @spec.stubs(:package_version).returns('1.0.0-alpha.1')
808
+ @spec.stubs(:valid_publish_config?).returns(true)
809
+ @spec.stubs(:publish_config_access).returns('restricted')
810
+ @spec.stubs(:registry).returns("@private:registry=some_private_registry")
811
+ assert_equal ['assert-npm-version-tag', 'npm publish --tag next --access restricted'], @spec.deploy_steps
611
812
  end
612
813
 
613
814
  test 'bundler installs take priority over yarn installs' do
@@ -663,14 +864,49 @@ module Shipit
663
864
  assert_equal ['yarn install --no-progress'], @spec.dependencies_steps
664
865
  end
665
866
 
666
- test '#publish_yarn_package checks if version tag exists, and then invokes npm publish script' do
867
+ test '#publish_npm_package checks if version tag exists, and then invokes npm publish script' do
667
868
  @spec.stubs(:yarn?).returns(true).at_least_once
668
- assert_equal ['assert-npm-version-tag', 'npm publish'], @spec.deploy_steps
869
+ @spec.stubs(:package_version).returns('1.0.0')
870
+ @spec.stubs(:valid_publish_config?).returns(true)
871
+ @spec.stubs(:publish_config_access).returns('restricted')
872
+ @spec.stubs(:registry).returns("@private:registry=some_private_registry")
873
+ assert_equal ['assert-npm-version-tag', 'npm publish --tag latest --access restricted'], @spec.deploy_steps
874
+ end
875
+
876
+ test '#publish_npm_package checks if version tag exists, generates npmrc, and then invokes npm publish script when enforce_publish_config? is true' do
877
+ @spec.stubs(:yarn?).returns(true).at_least_once
878
+ @spec.stubs(:package_version).returns('1.0.0')
879
+ @spec.stubs(:valid_publish_config?).returns(true)
880
+
881
+ @spec.stubs(:publish_config_access).returns('restricted')
882
+ @spec.stubs(:enforce_publish_config?).returns(true)
883
+ @spec.stubs(:registry).returns('fake')
884
+
885
+ generate_npmrc = 'generate-local-npmrc "fake"'
886
+ npm_publish = 'npm publish --tag latest --access restricted'
887
+ deploy_steps = ['assert-npm-version-tag', generate_npmrc, npm_publish]
888
+ assert_equal deploy_steps, @spec.deploy_steps
669
889
  end
670
890
 
671
891
  test 'yarn checklist takes precedence over npm checklist' do
672
892
  @spec.stubs(:yarn?).returns(true).at_least_once
673
893
  assert_match(/yarn version/, @spec.review_checklist[0])
674
894
  end
895
+
896
+ test "max_divergence_commits defaults to `nil" do
897
+ @spec.expects(:load_config).returns({})
898
+ assert_nil @spec.max_divergence_commits
899
+ end
900
+
901
+ test "max_divergence_age defaults to `nil` if `merge.max_divergence.age` cannot be parsed" do
902
+ @spec.expects(:load_config).returns(
903
+ 'merge' => {
904
+ 'max_divergence' => {
905
+ 'age' => 'badbadbad',
906
+ },
907
+ },
908
+ )
909
+ assert_nil @spec.max_divergence_age
910
+ end
675
911
  end
676
912
  end