shipit-engine 0.20.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -22,7 +22,7 @@ module Shipit
22
22
 
23
23
  response = begin
24
24
  create_deployment_on_github(author.github_api)
25
- rescue Octokit::NotFound, Octokit::Forbidden
25
+ rescue Octokit::ClientError
26
26
  raise if Shipit.github_api == author.github_api
27
27
  # If the deploy author didn't gave us the permission to create the deployment we falback the the main shipit
28
28
  # user.
@@ -10,7 +10,7 @@ module Shipit
10
10
  return if github_id?
11
11
  response = begin
12
12
  create_status_on_github(author.github_api)
13
- rescue Octokit::NotFound, Octokit::Forbidden
13
+ rescue Octokit::ClientError
14
14
  raise if Shipit.github_api == author.github_api
15
15
  # If the deploy author didn't gave us the permission to create the deployment we falback the the main shipit
16
16
  # user.
@@ -15,7 +15,7 @@ module Shipit
15
15
  end
16
16
 
17
17
  def bundle_path
18
- Rails.root.join('data/bundler')
18
+ Rails.root.join('data', 'bundler')
19
19
  end
20
20
  end
21
21
 
@@ -135,13 +135,17 @@ module Shipit
135
135
  end
136
136
 
137
137
  def required_statuses
138
- Array.wrap(config('ci', 'require'))
138
+ (Array.wrap(config('ci', 'require')) + blocking_statuses).uniq
139
139
  end
140
140
 
141
141
  def soft_failing_statuses
142
142
  Array.wrap(config('ci', 'allow_failures'))
143
143
  end
144
144
 
145
+ def blocking_statuses
146
+ Array.wrap(config('ci', 'blocking'))
147
+ end
148
+
145
149
  def pull_request_required_statuses
146
150
  if config('merge', 'require') || config('merge', 'ignore')
147
151
  Array.wrap(config('merge', 'require'))
@@ -167,6 +171,19 @@ module Shipit
167
171
  end
168
172
  end
169
173
 
174
+ def max_divergence_commits
175
+ config('merge', 'max_divergence', 'commits')
176
+ end
177
+
178
+ def max_divergence_age
179
+ if timeout = config('merge', 'max_divergence', 'age')
180
+ begin
181
+ Duration.parse(timeout)
182
+ rescue Duration::ParseError
183
+ end
184
+ end
185
+ end
186
+
170
187
  def review_checks
171
188
  config('review', 'checks') || []
172
189
  end
@@ -54,16 +54,7 @@ module Shipit
54
54
  end
55
55
 
56
56
  def coerce_task_definition(config)
57
- coerced_steps = Array(config['steps']).map do |command|
58
- should_prepend_bundle_exec?(command) ? bundle_exec(command) : command
59
- end
60
- config.merge('steps' => coerced_steps)
61
- end
62
-
63
- private
64
-
65
- def should_prepend_bundle_exec?(command)
66
- Shipit.automatically_prepend_bundle_exec && !command.start_with?('bundle exec')
57
+ config.merge('steps' => Array(config['steps']))
67
58
  end
68
59
  end
69
60
  end
@@ -2,6 +2,7 @@ module Shipit
2
2
  class DeploySpec
3
3
  class FileSystem < DeploySpec
4
4
  include NpmDiscovery
5
+ include LernaDiscovery
5
6
  include PypiDiscovery
6
7
  include RubygemsDiscovery
7
8
  include CapistranoDiscovery
@@ -33,11 +34,16 @@ module Shipit
33
34
  'require' => pull_request_required_statuses,
34
35
  'ignore' => pull_request_ignored_statuses,
35
36
  'revalidate_after' => revalidate_pull_requests_after.try!(:to_i),
37
+ 'max_divergence' => {
38
+ 'commits' => max_divergence_commits.try!(:to_i),
39
+ 'age' => max_divergence_age.try!(:to_i),
40
+ },
36
41
  },
37
42
  'ci' => {
38
43
  'hide' => hidden_statuses,
39
44
  'allow_failures' => soft_failing_statuses,
40
45
  'require' => required_statuses,
46
+ 'blocking' => blocking_statuses,
41
47
  },
42
48
  'machine' => {
43
49
  'environment' => discover_machine_env.merge(machine_env),
@@ -26,7 +26,7 @@ module Shipit
26
26
  private
27
27
 
28
28
  def discover_kubernetes
29
- return unless kube_config.present?
29
+ return if kube_config.blank?
30
30
 
31
31
  cmd = ["kubernetes-deploy"]
32
32
  if kube_config['template_dir']
@@ -0,0 +1,85 @@
1
+ require 'json'
2
+
3
+ module Shipit
4
+ class DeploySpec
5
+ module LernaDiscovery
6
+ def discover_dependencies_steps
7
+ discover_lerna_json || super
8
+ end
9
+
10
+ def discover_lerna_json
11
+ lerna_install if lerna?
12
+ end
13
+
14
+ def lerna_install
15
+ [js_command('install --no-progress'), 'node_modules/.bin/lerna bootstrap']
16
+ end
17
+
18
+ def discover_review_checklist
19
+ discover_lerna_checklist || super
20
+ end
21
+
22
+ def discover_lerna_checklist
23
+ if lerna?
24
+ [%(
25
+ <strong>Don't forget version and tag before publishing!</strong>
26
+ You can do this with:<br/>
27
+ <pre>
28
+ lerna publish --skip-npm
29
+ && git add -A
30
+ && git push --follow-tags
31
+ </pre>
32
+ )]
33
+ end
34
+ end
35
+
36
+ def lerna?
37
+ lerna_json.exist?
38
+ end
39
+
40
+ def lerna_json
41
+ file('lerna.json')
42
+ end
43
+
44
+ def lerna_version
45
+ lerna_config = lerna_json.read
46
+ JSON.parse(lerna_config)['version']
47
+ end
48
+
49
+ def discover_lerna_packages
50
+ publish_lerna_packages if lerna?
51
+ end
52
+
53
+ def discover_deploy_steps
54
+ discover_lerna_packages || super
55
+ end
56
+
57
+ def publish_lerna_packages
58
+ return publish_independent_packages if lerna_version == 'independent'
59
+ publish_fixed_version_packages
60
+ end
61
+
62
+ def publish_independent_packages
63
+ [
64
+ 'assert-lerna-independent-version-tags',
65
+ 'publish-lerna-independent-packages',
66
+ ]
67
+ end
68
+
69
+ def publish_fixed_version_packages
70
+ check_tags = 'assert-lerna-fixed-version-tag'
71
+ # `yarn publish` requires user input, so always use npm.
72
+ version = lerna_version
73
+ publish =
74
+ "node_modules/.bin/lerna publish " \
75
+ "--yes " \
76
+ "--skip-git " \
77
+ "--repo-version #{version} " \
78
+ "--force-publish=* " \
79
+ "--npm-tag #{dist_tag(version)}"
80
+
81
+ [check_tags, publish]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -3,6 +3,12 @@ require 'json'
3
3
  module Shipit
4
4
  class DeploySpec
5
5
  module NpmDiscovery
6
+ # https://docs.npmjs.com/cli/publish
7
+ PUBLIC = 'public'.freeze
8
+ PRIVATE = 'restricted'.freeze
9
+ VALID_ACCESS = [PUBLIC, PRIVATE].freeze
10
+ NPM_REGISTRY = "https://registry.npmjs.org/".freeze
11
+
6
12
  def discover_dependencies_steps
7
13
  discover_package_json || super
8
14
  end
@@ -20,13 +26,19 @@ module Shipit
20
26
  end
21
27
 
22
28
  def discover_yarn_checklist
23
- [%(<strong>Don't forget version and tag before publishing!</strong> You can do this with:<br/>
24
- yarn version --new-version <strong>&lt;major|minor|patch&gt;</strong> && git push --tags</pre>)] if yarn?
29
+ if yarn?
30
+ [%(<strong>Don't forget version and tag before publishing!</strong> You can do this with:<br/>
31
+ yarn version --new-version <strong>&lt;major|minor|patch&gt;</strong>
32
+ && git push --follow-tags</pre>)]
33
+ end
25
34
  end
26
35
 
27
36
  def discover_npm_checklist
28
- [%(<strong>Don't forget version and tag before publishing!</strong> You can do this with:<br/>
29
- npm version <strong>&lt;major|minor|patch&gt;</strong> && git push --tags</pre>)] if npm?
37
+ if npm?
38
+ [%(<strong>Don't forget version and tag before publishing!</strong> You can do this with:<br/>
39
+ npm version <strong>&lt;major|minor|patch&gt;</strong>
40
+ && git push --follow-tags</pre>)]
41
+ end
30
42
  end
31
43
 
32
44
  def npm?
@@ -40,10 +52,23 @@ module Shipit
40
52
  JSON.parse(file.read)['private'].blank?
41
53
  end
42
54
 
55
+ def dist_tag(version)
56
+ # Pre-release SemVer tags such as 'beta', 'alpha', 'rc' and 'next'
57
+ # are treated as 'next' npm dist-tags.
58
+ # An 1.0.0-beta.1 would be installable using both:
59
+ # `yarn add package@1.0.0-beta.1` and `yarn add package@next`
60
+ return 'next' if ['-beta', '-alpha', '-rc', '-next'].any? { |tag| version.include? tag }
61
+ 'latest'
62
+ end
63
+
43
64
  def package_json
44
65
  file('package.json')
45
66
  end
46
67
 
68
+ def package_json_contents
69
+ @package_json_contents ||= JSON.parse(package_json.read)
70
+ end
71
+
47
72
  def yarn?
48
73
  yarn_lock.exist? && public?
49
74
  end
@@ -60,11 +85,84 @@ module Shipit
60
85
  discover_npm_package || super
61
86
  end
62
87
 
88
+ def package_name
89
+ package_json_contents['name']
90
+ end
91
+
92
+ def package_version
93
+ package_json_contents['version']
94
+ end
95
+
96
+ def publish_config
97
+ package_json_contents['publishConfig']
98
+ end
99
+
100
+ def publish_config_access
101
+ config = publish_config
102
+
103
+ # default to private deploy when we enforce a publishConfig
104
+ if enforce_publish_config?
105
+ return PRIVATE if config.blank?
106
+ config['access'] || PRIVATE
107
+ end
108
+
109
+ return PUBLIC if config.blank?
110
+ config['access'] || PUBLIC
111
+ end
112
+
113
+ def scoped_package?
114
+ return false if Shipit.npm_org_scope.nil?
115
+ package_name.start_with?(Shipit.npm_org_scope)
116
+ end
117
+
118
+ def enforce_publish_config?
119
+ enforce = Shipit.enforce_publish_config
120
+ return false if enforce.nil? || enforce.to_s == "0"
121
+ true
122
+ end
123
+
124
+ def valid_publish_config?
125
+ return true unless enforce_publish_config?
126
+ return false if Shipit.private_npm_registry.nil?
127
+ return false if publish_config.blank?
128
+ return true if publish_config_access == PUBLIC
129
+
130
+ valid_publish_config_access? && private_scoped_package?
131
+ end
132
+
133
+ def valid_publish_config_access?
134
+ VALID_ACCESS.include?(publish_config_access)
135
+ end
136
+
137
+ # ensure private packages are scoped
138
+ def private_scoped_package?
139
+ publish_config_access == PRIVATE && scoped_package?
140
+ end
141
+
142
+ def local_npmrc
143
+ file(".npmrc")
144
+ end
145
+
146
+ def registry
147
+ scope = Shipit.npm_org_scope
148
+ prefix = scoped_package? ? "#{scope}:registry" : "registry"
149
+
150
+ if publish_config_access == PUBLIC
151
+ return "#{prefix}=#{NPM_REGISTRY}"
152
+ end
153
+
154
+ "#{prefix}=#{Shipit.private_npm_registry}"
155
+ end
156
+
63
157
  def publish_npm_package
158
+ return ['misconfigured-npm-publish-config'] unless valid_publish_config?
159
+
160
+ generate_npmrc = "generate-local-npmrc \"#{registry}\""
64
161
  check_tags = 'assert-npm-version-tag'
65
162
  # `yarn publish` requires user input, so always use npm.
66
- publish = 'npm publish'
163
+ publish = "npm publish --tag #{dist_tag(package_version)} --access #{publish_config_access}"
67
164
 
165
+ return [check_tags, generate_npmrc, publish] if enforce_publish_config?
68
166
  [check_tags, publish]
69
167
  end
70
168
 
@@ -14,8 +14,10 @@ module Shipit
14
14
  end
15
15
 
16
16
  def discover_pypi_checklist
17
- [%(<strong>Don't forget to add a tag before deploying!</strong> You can do this with:
18
- git tag -a -m "Version <strong>x.y.z</strong>" v<strong>x.y.z</strong> && git push --tags)] if egg?
17
+ if egg?
18
+ [%(<strong>Don't forget to add a tag before deploying!</strong> You can do this with:
19
+ git tag -a -m "Version <strong>x.y.z</strong>" v<strong>x.y.z</strong> && git push --tags)]
20
+ end
19
21
  end
20
22
 
21
23
  def egg?
@@ -14,8 +14,10 @@ module Shipit
14
14
  end
15
15
 
16
16
  def discover_gem_checklist
17
- [%(<strong>Don't forget to add a tag before deploying!</strong> You can do this with:
18
- git tag v<strong>x.y.z</strong> && git push --tags)] if gem?
17
+ if gem?
18
+ [%(<strong>Don't forget to add a tag before deploying!</strong> You can do this with:
19
+ git tag v<strong>x.y.z</strong> && git push --tags)]
20
+ end
19
21
  end
20
22
 
21
23
  def gem?
@@ -23,7 +23,7 @@ module Shipit
23
23
  raise ParseError, "not a duration: #{value.inspect}"
24
24
  end
25
25
  parts = []
26
- UNITS.values.each do |unit|
26
+ UNITS.each_value do |unit|
27
27
  if value = match[unit]
28
28
  parts << [unit, value.to_i]
29
29
  end
@@ -9,7 +9,7 @@ module Shipit
9
9
 
10
10
  def refresh_status
11
11
  Rails.cache.write(CACHE_KEY, Shipit.github_api.github_status)
12
- rescue Net::OpenTimeout, Octokit::ServerError
12
+ rescue Faraday::Error, Octokit::ServerError
13
13
  end
14
14
  end
15
15
  end
@@ -31,8 +31,8 @@ module Shipit
31
31
  serialize :events, Shipit::CSVSerializer
32
32
 
33
33
  scope :global, -> { where(stack_id: nil) }
34
- scope :scoped_to, -> (stack) { where(stack_id: stack.id) }
35
- scope :for_stack, -> (stack_id) { where(stack_id: [nil, stack_id]) }
34
+ scope :scoped_to, ->(stack) { where(stack_id: stack.id) }
35
+ scope :for_stack, ->(stack_id) { where(stack_id: [nil, stack_id]) }
36
36
 
37
37
  class << self
38
38
  def emit(event, stack, payload)
@@ -80,9 +80,8 @@ module Shipit
80
80
  end
81
81
 
82
82
  def purge_old_deliveries!(keep: DELIVERIES_LOG_SIZE)
83
- if cut_off_time = deliveries.order(created_at: :desc).limit(1).offset(keep).pluck(:created_at).first
84
- deliveries.where('created_at <= ?', cut_off_time).delete_all
85
- end
83
+ delivery_ids = deliveries.sent.order(id: :desc).offset(keep).pluck(:id)
84
+ deliveries.where(id: delivery_ids).delete_all
86
85
  end
87
86
 
88
87
  private
@@ -2,7 +2,7 @@ module Shipit
2
2
  class OutputChunk < ActiveRecord::Base
3
3
  belongs_to :task
4
4
 
5
- scope :tail, -> (start) { order(id: :asc).where('id > ?', start || 0) }
5
+ scope :tail, ->(start) { order(id: :asc).where('id > ?', start || 0) }
6
6
 
7
7
  def text=(string)
8
8
  super(string.force_encoding(Encoding::UTF_8).scrub)
@@ -4,7 +4,7 @@ module Shipit
4
4
 
5
5
  WAITING_STATUSES = %w(fetching pending).freeze
6
6
  QUEUED_STATUSES = %w(pending revalidating).freeze
7
- REJECTION_REASONS = %w(ci_failing merge_conflict).freeze
7
+ REJECTION_REASONS = %w(ci_failing merge_conflict requires_rebase).freeze
8
8
  InvalidTransition = Class.new(StandardError)
9
9
  NotReady = Class.new(StandardError)
10
10
 
@@ -37,6 +37,7 @@ module Shipit
37
37
 
38
38
  belongs_to :stack
39
39
  belongs_to :head, class_name: 'Shipit::Commit', optional: true
40
+ belongs_to :base_commit, class_name: 'Shipit::Commit', optional: true
40
41
  belongs_to :merge_requested_by, class_name: 'Shipit::User', optional: true
41
42
  has_one :merge_commit, class_name: 'Shipit::Commit'
42
43
 
@@ -112,7 +113,7 @@ module Shipit
112
113
  when %r{\Ahttps://#{Regexp.escape(Shipit.github_domain)}/([^/]+)/([^/]+)/pull/(\d+)}
113
114
  return unless $1.downcase == stack.repo_owner.downcase
114
115
  return unless $2.downcase == stack.repo_name.downcase
115
- return $3.to_i
116
+ $3.to_i
116
117
  end
117
118
  end
118
119
 
@@ -146,7 +147,8 @@ module Shipit
146
147
 
147
148
  def reject_unless_mergeable!
148
149
  return reject!('merge_conflict') if merge_conflict?
149
- return reject!('ci_failing') unless all_status_checks_passed?
150
+ return reject!('ci_failing') if any_status_checks_failed?
151
+ return reject!('requires_rebase') if stale?
150
152
  false
151
153
  end
152
154
 
@@ -154,10 +156,6 @@ module Shipit
154
156
  raise InvalidTransition unless pending?
155
157
 
156
158
  raise NotReady if not_mergeable_yet?
157
- if need_revalidation?
158
- revalidate!
159
- return false
160
- end
161
159
 
162
160
  Shipit.github_api.merge_pull_request(
163
161
  stack.github_repo_name,
@@ -184,9 +182,15 @@ module Shipit
184
182
  end
185
183
 
186
184
  def all_status_checks_passed?
185
+ return false unless head
187
186
  StatusChecker.new(head, head.statuses, stack.cached_deploy_spec).success?
188
187
  end
189
188
 
189
+ def any_status_checks_failed?
190
+ status = StatusChecker.new(head, head.statuses, stack.cached_deploy_spec)
191
+ status.failure? || status.error?
192
+ end
193
+
190
194
  def waiting?
191
195
  WAITING_STATUSES.include?(merge_status)
192
196
  end
@@ -221,6 +225,7 @@ module Shipit
221
225
  update!(github_pull_request: Shipit.github_api.pull_request(stack.github_repo_name, number))
222
226
  head.refresh_statuses!
223
227
  fetched! if fetching?
228
+ @comparison = nil
224
229
  end
225
230
 
226
231
  def github_pull_request=(github_pull_request)
@@ -234,6 +239,8 @@ module Shipit
234
239
  self.branch = github_pull_request.head.ref
235
240
  self.head = find_or_create_commit_from_github_by_sha!(github_pull_request.head.sha, detached: true)
236
241
  self.merged_at = github_pull_request.merged_at
242
+ self.base_ref = github_pull_request.base.ref
243
+ self.base_commit = find_or_create_commit_from_github_by_sha!(github_pull_request.base.sha, detached: true)
237
244
  end
238
245
 
239
246
  def merge_message
@@ -241,16 +248,30 @@ module Shipit
241
248
  "#{title}\n\nMerge-Requested-By: #{merge_requested_by.login}\n"
242
249
  end
243
250
 
244
- private
245
-
246
- if Rails.gem_version >= Gem::Version.new('5.1.0.beta1')
247
- def record_merge_status_change
248
- @merge_status_changed ||= saved_change_to_attribute?(:merge_status)
251
+ def stale?
252
+ return false unless base_commit
253
+ spec = stack.cached_deploy_spec
254
+ if max_branch_age = spec.max_divergence_age
255
+ return true if Time.now.utc - head.committed_at > max_branch_age
249
256
  end
250
- else
251
- def record_merge_status_change
252
- @merge_status_changed ||= merge_status_changed?
257
+ if commit_count_limit = spec.max_divergence_commits
258
+ return true if comparison.behind_by > commit_count_limit
253
259
  end
260
+ false
261
+ end
262
+
263
+ def comparison
264
+ @comparison ||= Shipit.github_api.compare(
265
+ stack.github_repo_name,
266
+ base_ref,
267
+ head.sha,
268
+ )
269
+ end
270
+
271
+ private
272
+
273
+ def record_merge_status_change
274
+ @merge_status_changed ||= saved_change_to_attribute?(:merge_status)
254
275
  end
255
276
 
256
277
  def emit_hooks