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
@@ -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