shipit-engine 0.20.0 → 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 (110) 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/bundler_discovery.rb +1 -10
  28. data/app/models/shipit/deploy_spec/file_system.rb +6 -0
  29. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +1 -1
  30. data/app/models/shipit/deploy_spec/lerna_discovery.rb +85 -0
  31. data/app/models/shipit/deploy_spec/npm_discovery.rb +103 -5
  32. data/app/models/shipit/deploy_spec/pypi_discovery.rb +4 -2
  33. data/app/models/shipit/deploy_spec/rubygems_discovery.rb +4 -2
  34. data/app/models/shipit/deploy_spec.rb +19 -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 +37 -16
  40. data/app/models/shipit/stack.rb +17 -11
  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/command.rb +11 -3
  63. data/lib/shipit/engine.rb +0 -4
  64. data/lib/shipit/stack_commands.rb +3 -1
  65. data/lib/shipit/version.rb +1 -1
  66. data/lib/shipit.rb +15 -23
  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 +7 -1
  97. data/test/models/pull_request_test.rb +78 -24
  98. data/test/models/stacks_test.rb +23 -19
  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 -224
  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
  110. data/test/dummy/db/test.sqlite3-journal +0 -0
@@ -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?
@@ -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
@@ -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
 
@@ -102,7 +103,7 @@ module Shipit
102
103
  end
103
104
 
104
105
  def self.schedule_merges
105
- Shipit::Stack.where(id: pending.uniq.pluck(:stack_id)).find_each(&:schedule_merges)
106
+ Shipit::Stack.where(merge_queue_enabled: true).find_each(&:schedule_merges)
106
107
  end
107
108
 
108
109
  def self.extract_number(stack, number_or_url)
@@ -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
@@ -44,6 +44,7 @@ module Shipit
44
44
 
45
45
  before_validation :update_defaults
46
46
  before_destroy :clear_local_files
47
+ before_save :set_locked_since
47
48
  after_commit :emit_lock_hooks
48
49
  after_commit :emit_added_hooks, on: :create
49
50
  after_commit :emit_updated_hooks, on: :update
@@ -198,14 +199,14 @@ module Shipit
198
199
  commits.reachable.first.try!(:sha)
199
200
  end
200
201
 
201
- def merge_status(backlog_leniency_factor: 1.5)
202
+ def merge_status(backlog_leniency_factor: 2.0)
202
203
  return 'locked' if locked?
203
204
  return 'failure' if %w(failure error).freeze.include?(branch_status)
204
205
  return 'backlogged' if backlogged?(backlog_leniency_factor: backlog_leniency_factor)
205
206
  'success'
206
207
  end
207
208
 
208
- def backlogged?(backlog_leniency_factor: 1.5)
209
+ def backlogged?(backlog_leniency_factor: 2.0)
209
210
  maximum_commits_per_deploy && (undeployed_commits_count > maximum_commits_per_deploy * backlog_leniency_factor)
210
211
  end
211
212
 
@@ -255,11 +256,7 @@ module Shipit
255
256
  end
256
257
 
257
258
  def last_deployed_commit
258
- if deploy = last_successful_deploy
259
- deploy.until_commit
260
- else
261
- NoDeployedCommit
262
- end
259
+ last_successful_deploy.try!(:until_commit) || NoDeployedCommit
263
260
  end
264
261
 
265
262
  def deployable?
@@ -287,7 +284,7 @@ module Shipit
287
284
  end
288
285
 
289
286
  def base_path
290
- Rails.root.join('data/stacks', repo_owner, repo_name, environment)
287
+ Rails.root.join('data', 'stacks', repo_owner, repo_name, environment)
291
288
  end
292
289
 
293
290
  def deploys_path
@@ -362,7 +359,6 @@ module Shipit
362
359
 
363
360
  def lock(reason, user)
364
361
  params = {lock_reason: reason, lock_author: user}
365
- params[:locked_since] = Time.current if locked_since.nil?
366
362
  update!(params)
367
363
  end
368
364
 
@@ -384,8 +380,8 @@ module Shipit
384
380
  end
385
381
 
386
382
  delegate :plugins, :task_definitions, :hidden_statuses, :required_statuses, :soft_failing_statuses,
387
- :deploy_variables, :filter_task_envs, :filter_deploy_envs, :maximum_commits_per_deploy,
388
- :pause_between_deploys, to: :cached_deploy_spec
383
+ :blocking_statuses, :deploy_variables, :filter_task_envs, :filter_deploy_envs,
384
+ :maximum_commits_per_deploy, :pause_between_deploys, to: :cached_deploy_spec
389
385
 
390
386
  def monitoring?
391
387
  monitoring.present?
@@ -486,6 +482,16 @@ module Shipit
486
482
  self.branch = 'master' if branch.blank?
487
483
  end
488
484
 
485
+ def set_locked_since
486
+ return unless lock_reason_changed?
487
+
488
+ if lock_reason.blank?
489
+ self.locked_since = nil
490
+ else
491
+ self.locked_since ||= Time.now
492
+ end
493
+ end
494
+
489
495
  def schedule_merges_if_necessary
490
496
  if previous_changes.include?('lock_reason') && previous_changes['lock_reason'].last.blank?
491
497
  schedule_merges
@@ -37,6 +37,10 @@ module Shipit
37
37
  commit.hidden_statuses.include?(context)
38
38
  end
39
39
 
40
+ def blocking?
41
+ !success? && commit.blocking_statuses.include?(context)
42
+ end
43
+
40
44
  def required?
41
45
  commit.required_statuses.include?(context)
42
46
  end
@@ -52,6 +52,10 @@ module Shipit
52
52
  true
53
53
  end
54
54
 
55
+ def blocking?
56
+ statuses.any?(&:blocking?)
57
+ end
58
+
55
59
  private
56
60
 
57
61
  def reject_hidden(statuses)
@@ -7,13 +7,14 @@ module Shipit
7
7
  PRESENCE_CHECK_TIMEOUT = 15
8
8
  ACTIVE_STATUSES = %w(pending running aborting).freeze
9
9
  COMPLETED_STATUSES = %w(success error failed flapping aborted).freeze
10
- UNSUCCESSFUL_STATUSES = %w(error failed aborted flapping).freeze
10
+ UNSUCCESSFUL_STATUSES = %w(error failed aborted flapping timedout).freeze
11
11
 
12
12
  attr_accessor :pid
13
13
 
14
14
  belongs_to :deploy, foreign_key: :parent_id, required: false # required for fixtures
15
15
 
16
16
  belongs_to :user, optional: true
17
+ belongs_to :aborted_by, class_name: 'User', optional: true
17
18
  belongs_to :stack, counter_cache: true
18
19
  belongs_to :until_commit, class_name: 'Commit'
19
20
  belongs_to :since_commit, class_name: 'Commit'
@@ -56,11 +57,11 @@ module Shipit
56
57
  task.started_at ||= Time.now.utc
57
58
  end
58
59
 
59
- before_transition any => %i(success failed error) do |task|
60
+ before_transition any => %i(success failed error timedout) do |task|
60
61
  task.ended_at ||= Time.now.utc
61
62
  end
62
63
 
63
- after_transition any => %i(success failed error) do |task|
64
+ after_transition any => %i(success failed error timedout) do |task|
64
65
  task.async_refresh_deployed_revision
65
66
  end
66
67
 
@@ -88,6 +89,10 @@ module Shipit
88
89
  transition all => :error
89
90
  end
90
91
 
92
+ event :giveup do # :timeout would cause a name clash
93
+ transition all => :timedout
94
+ end
95
+
91
96
  event :aborting do
92
97
  transition all - %i(aborted) => :aborting
93
98
  end
@@ -97,7 +102,7 @@ module Shipit
97
102
  end
98
103
 
99
104
  event :flap do
100
- transition %i(failed error success) => :flapping
105
+ transition %i(failed error timedout success) => :flapping
101
106
  end
102
107
 
103
108
  state :pending
@@ -105,6 +110,7 @@ module Shipit
105
110
  state :failed
106
111
  state :success
107
112
  state :error
113
+ state :timedout
108
114
  state :aborting
109
115
  state :aborted
110
116
  state :flapping
@@ -114,15 +120,21 @@ module Shipit
114
120
  status.in?(ACTIVE_STATUSES)
115
121
  end
116
122
 
117
- def report_failure!(_error)
123
+ def report_failure!(error)
118
124
  reload
119
125
  if aborting?
120
126
  aborted!
127
+ elsif error.respond_to?(:exit_code) && Shipit.timeout_exit_codes.include?(error.exit_code)
128
+ giveup!
121
129
  else
122
130
  failure!
123
131
  end
124
132
  end
125
133
 
134
+ def report_timeout!(_error)
135
+ giveup!
136
+ end
137
+
126
138
  def report_error!(error)
127
139
  write("#{error.class}: #{error.message}\n\t#{error.backtrace.join("\n\t")}\n")
128
140
  error!
@@ -177,7 +189,7 @@ module Shipit
177
189
  def output
178
190
  gzip = self[:gzip_output]
179
191
 
180
- if gzip.nil? || gzip.empty?
192
+ if gzip.blank?
181
193
  ''
182
194
  else
183
195
  ActiveSupport::Gzip.decompress(gzip)
@@ -236,8 +248,8 @@ module Shipit
236
248
  end
237
249
  end
238
250
 
239
- def abort!(rollback_once_aborted: false)
240
- update!(rollback_once_aborted: rollback_once_aborted)
251
+ def abort!(rollback_once_aborted: false, aborted_by:)
252
+ update!(rollback_once_aborted: rollback_once_aborted, aborted_by_id: aborted_by.id)
241
253
 
242
254
  if alive?
243
255
  aborting