shipit-engine 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/shipit/stacks.js.coffee +15 -1
  3. data/app/assets/stylesheets/_base/_icons.scss +18 -0
  4. data/app/assets/stylesheets/_base/_status-items.scss +28 -0
  5. data/app/assets/stylesheets/_pages/_commits.scss +1 -5
  6. data/app/assets/stylesheets/_pages/_deploy.scss +60 -3
  7. data/app/controllers/concerns/shipit/authentication.rb +1 -1
  8. data/app/controllers/shipit/merge_status_controller.rb +2 -0
  9. data/app/controllers/shipit/release_statuses_controller.rb +36 -0
  10. data/app/jobs/shipit/append_delayed_release_status_job.rb +17 -0
  11. data/app/jobs/shipit/cache_deploy_spec_job.rb +2 -0
  12. data/app/jobs/shipit/clear_git_cache_job.rb +1 -1
  13. data/app/jobs/shipit/create_release_statuses_job.rb +11 -0
  14. data/app/jobs/shipit/deferred_touch_job.rb +2 -0
  15. data/app/jobs/shipit/deliver_hook_job.rb +1 -0
  16. data/app/jobs/shipit/merge_pull_requests_job.rb +2 -0
  17. data/app/jobs/shipit/perform_commit_checks_job.rb +2 -0
  18. data/app/jobs/shipit/perform_task_job.rb +2 -4
  19. data/app/jobs/shipit/refresh_pull_request_job.rb +2 -0
  20. data/app/models/shipit/anonymous_user.rb +1 -1
  21. data/app/models/shipit/commit.rb +36 -3
  22. data/app/models/shipit/deploy.rb +43 -0
  23. data/app/models/shipit/deploy_spec.rb +16 -2
  24. data/app/models/shipit/deploy_spec/bundler_discovery.rb +5 -1
  25. data/app/models/shipit/deploy_spec/file_system.rb +4 -0
  26. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +1 -1
  27. data/app/models/shipit/ephemeral_commit_checks.rb +2 -4
  28. data/app/models/shipit/hook.rb +36 -3
  29. data/app/models/shipit/pull_request.rb +4 -2
  30. data/app/models/shipit/release_status.rb +41 -0
  31. data/app/models/shipit/rollback.rb +9 -0
  32. data/app/models/shipit/stack.rb +4 -9
  33. data/app/models/shipit/status/common.rb +4 -0
  34. data/app/models/shipit/status/group.rb +2 -1
  35. data/app/models/shipit/status/missing.rb +4 -0
  36. data/app/models/shipit/status/unknown.rb +15 -0
  37. data/app/models/shipit/task.rb +4 -0
  38. data/app/models/shipit/user.rb +16 -3
  39. data/app/serializers/shipit/stack_serializer.rb +1 -1
  40. data/app/views/shipit/deploys/_deploy.html.erb +18 -2
  41. data/config/locales/en.yml +3 -0
  42. data/config/routes.rb +2 -0
  43. data/db/migrate/20180802172632_allow_commit_without_author.rb +6 -0
  44. data/db/migrate/20180906083930_create_release_statuses.rb +21 -0
  45. data/lib/shipit.rb +5 -0
  46. data/lib/shipit/command.rb +14 -18
  47. data/lib/shipit/deploy_commands.rb +0 -4
  48. data/lib/shipit/engine.rb +1 -1
  49. data/lib/shipit/first_parent_commits_iterator.rb +1 -1
  50. data/lib/shipit/flock.rb +43 -0
  51. data/lib/shipit/github_app.rb +5 -3
  52. data/lib/shipit/rollback_commands.rb +6 -0
  53. data/lib/shipit/task_commands.rb +1 -5
  54. data/lib/shipit/version.rb +1 -1
  55. data/test/controllers/release_statuses_controller_test.rb +23 -0
  56. data/test/dummy/db/schema.rb +18 -3
  57. data/test/dummy/db/seeds.rb +4 -0
  58. data/test/fixtures/shipit/commits.yml +13 -0
  59. data/test/fixtures/shipit/release_statuses.yml +16 -0
  60. data/test/fixtures/shipit/stacks.yml +4 -0
  61. data/test/jobs/append_delayed_release_status_job_test.rb +25 -0
  62. data/test/jobs/cache_deploy_spec_job_test.rb +1 -2
  63. data/test/jobs/emit_event_job_test.rb +1 -1
  64. data/test/jobs/github_sync_job_test.rb +1 -0
  65. data/test/models/commits_test.rb +54 -1
  66. data/test/models/deploy_spec_test.rb +83 -11
  67. data/test/models/deploys_test.rb +52 -0
  68. data/test/models/hook_test.rb +1 -28
  69. data/test/models/pull_request_test.rb +19 -0
  70. data/test/models/release_statuses_test.rb +28 -0
  71. data/test/models/rollbacks_test.rb +2 -0
  72. data/test/models/stacks_test.rb +1 -1
  73. data/test/test_helper.rb +5 -0
  74. data/test/unit/rollback_commands_test.rb +35 -0
  75. metadata +121 -104
@@ -9,6 +9,7 @@ module Shipit
9
9
  after_transition to: :success, do: :schedule_merges
10
10
  after_transition to: :success, do: :update_undeployed_commits_count
11
11
  after_transition to: :aborted, do: :trigger_revert_if_required
12
+ after_transition any => any, do: :update_release_status
12
13
  after_transition any => any, do: :update_commit_deployments
13
14
  after_transition any => any, do: :update_last_deploy_time
14
15
  end
@@ -33,6 +34,7 @@ module Shipit
33
34
 
34
35
  before_create :denormalize_commit_stats
35
36
  after_create :create_commit_deployments
37
+ after_create :update_release_status
36
38
  after_commit :broadcast_update
37
39
 
38
40
  delegate :broadcast_update, :filter_deploy_envs, to: :stack
@@ -143,6 +145,21 @@ module Shipit
143
145
  confirmations.abs >= CONFIRMATIONS_REQUIRED
144
146
  end
145
147
 
148
+ delegate :last_release_status, to: :until_commit
149
+ def append_release_status(state, description, user: self.user)
150
+ status = until_commit.create_release_status!(
151
+ state,
152
+ user: user.presence,
153
+ target_url: permalink,
154
+ description: description,
155
+ )
156
+ status
157
+ end
158
+
159
+ def permalink
160
+ Shipit::Engine.routes.url_helpers.stack_deploy_url(stack, self)
161
+ end
162
+
146
163
  private
147
164
 
148
165
  def create_commit_deployments
@@ -151,6 +168,32 @@ module Shipit
151
168
  end
152
169
  end
153
170
 
171
+ def update_release_status
172
+ return unless stack.release_status?
173
+
174
+ case status
175
+ when 'pending'
176
+ append_release_status('pending', "A deploy was triggered on #{stack.environment}")
177
+ when 'failed', 'error', 'timedout'
178
+ append_release_status('error', "The deploy on #{stack.environment} did not succeed (#{status})")
179
+ when 'aborted', 'aborting'
180
+ append_release_status('failure', "The deploy on #{stack.environment} was canceled")
181
+ when 'success'
182
+ delay = stack.release_status_delay
183
+ if delay.zero?
184
+ append_release_status('success', "The deploy on #{stack.environment} succeeded")
185
+ elsif delay.positive?
186
+ append_release_status('pending', "The deploy on #{stack.environment} succeeded")
187
+ AppendDelayedReleaseStatusJob.set(wait: delay).perform_later(
188
+ self,
189
+ cursor: until_commit.release_statuses.last,
190
+ status: 'success',
191
+ description: "No issue were signaled after #{delay}",
192
+ )
193
+ end
194
+ end
195
+ end
196
+
154
197
  def update_commit_deployments
155
198
  commit_deployments.append_status(status)
156
199
  end
@@ -67,6 +67,20 @@ module Shipit
67
67
  config('deploy', 'max_commits') { 8 }
68
68
  end
69
69
 
70
+ def release_status?
71
+ !!release_status_context
72
+ end
73
+
74
+ def release_status_context
75
+ config('status', 'context')
76
+ end
77
+
78
+ def release_status_delay
79
+ if delay = config('status', 'delay') { config('deploy', 'interval') }
80
+ Duration.parse(delay)
81
+ end
82
+ end
83
+
70
84
  def pause_between_deploys
71
85
  Duration.parse(config('deploy', 'interval') { 0 })
72
86
  end
@@ -131,7 +145,7 @@ module Shipit
131
145
  end
132
146
 
133
147
  def hidden_statuses
134
- Array.wrap(config('ci', 'hide'))
148
+ Array.wrap(config('ci', 'hide')) + [release_status_context].compact
135
149
  end
136
150
 
137
151
  def required_statuses
@@ -161,7 +175,7 @@ module Shipit
161
175
 
162
176
  def pull_request_ignored_statuses
163
177
  if config('merge', 'require') || config('merge', 'ignore')
164
- Array.wrap(config('merge', 'ignore'))
178
+ Array.wrap(config('merge', 'ignore')) + [release_status_context].compact
165
179
  else
166
180
  soft_failing_statuses | hidden_statuses
167
181
  end
@@ -19,8 +19,12 @@ module Shipit
19
19
  end
20
20
  end
21
21
 
22
+ def discover_machine_env
23
+ super.merge('BUNDLE_PATH' => bundle_path.to_s)
24
+ end
25
+
22
26
  def bundle_install
23
- bundle = %(bundle check --path=#{bundle_path} || bundle install #{frozen_flag} --path=#{bundle_path} --retry=2)
27
+ bundle = %(bundle install #{frozen_flag} --jobs 4 --path #{bundle_path} --retry 2)
24
28
  bundle += " --without=#{bundler_without.join(':')}" unless bundler_without.empty?
25
29
  [remove_ruby_version_from_gemfile, bundle]
26
30
  end
@@ -57,6 +57,10 @@ module Shipit
57
57
  'checks' => review_checks,
58
58
  },
59
59
  'plugins' => plugins,
60
+ 'status' => {
61
+ 'context' => release_status_context,
62
+ 'delay' => release_status_delay,
63
+ },
60
64
  'dependencies' => {'override' => dependencies_steps},
61
65
  'deploy' => {
62
66
  'override' => deploy_steps,
@@ -17,7 +17,7 @@ module Shipit
17
17
  'description' => "Simulates a rollout of Kubernetes deployments by using kubernetes-restart utility",
18
18
  'steps' => [kubernetes_restart_cmd],
19
19
  },
20
- }
20
+ }.merge!(super)
21
21
  else
22
22
  super
23
23
  end
@@ -14,10 +14,8 @@ module Shipit
14
14
  commands = StackCommands.new(stack)
15
15
  commands.with_temporary_working_directory(commit: commit) do |directory|
16
16
  deploy_spec = DeploySpec::FileSystem.new(directory, stack.environment)
17
- Bundler.with_clean_env do
18
- capture_all(build_commands(deploy_spec.dependencies_steps, chdir: directory))
19
- capture_all(build_commands(deploy_spec.review_checks, chdir: directory))
20
- end
17
+ capture_all(build_commands(deploy_spec.dependencies_steps, chdir: directory))
18
+ capture_all(build_commands(deploy_spec.review_checks, chdir: directory))
21
19
  end
22
20
  self
23
21
  rescue Command::Error
@@ -1,5 +1,38 @@
1
1
  module Shipit
2
2
  class Hook < ActiveRecord::Base
3
+ class DeliverySpec
4
+ def initialize(event:, url:, content_type:, payload:)
5
+ @event = event
6
+ @url = url
7
+ @content_type = content_type
8
+ @payload = payload
9
+ end
10
+
11
+ def send!
12
+ http.post(url, payload)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :event, :url, :content_type, :payload
18
+
19
+ def http
20
+ Faraday::Connection.new do |connection|
21
+ connection.headers = headers
22
+ connection.adapter Faraday.default_adapter
23
+ end
24
+ end
25
+
26
+ def headers
27
+ {
28
+ 'User-Agent' => 'Shipit Webhook',
29
+ 'Content-Type' => content_type,
30
+ 'X-Shipit-Event' => event,
31
+ 'Accept' => '*/*',
32
+ }
33
+ end
34
+ end
35
+
3
36
  default_scope { order :id }
4
37
 
5
38
  DELIVERIES_LOG_SIZE = 500
@@ -71,12 +104,12 @@ module Shipit
71
104
  end
72
105
 
73
106
  def deliver!(event, payload)
74
- deliveries.create!(
75
- event: event,
107
+ DeliverHookJob.perform_later(
108
+ event: event.to_s,
76
109
  url: delivery_url,
77
110
  content_type: CONTENT_TYPES[content_type],
78
111
  payload: serialize_payload(payload),
79
- ).schedule!
112
+ )
80
113
  end
81
114
 
82
115
  def purge_old_deliveries!(keep: DELIVERIES_LOG_SIZE)
@@ -2,6 +2,8 @@ module Shipit
2
2
  class PullRequest < ApplicationRecord
3
3
  include DeferredTouch
4
4
 
5
+ MERGE_REQUEST_FIELD = 'Merge-Requested-By'.freeze
6
+
5
7
  WAITING_STATUSES = %w(fetching pending).freeze
6
8
  QUEUED_STATUSES = %w(pending revalidating).freeze
7
9
  REJECTION_REASONS = %w(ci_failing merge_conflict requires_rebase).freeze
@@ -188,7 +190,7 @@ module Shipit
188
190
 
189
191
  def any_status_checks_failed?
190
192
  status = StatusChecker.new(head, head.statuses, stack.cached_deploy_spec)
191
- status.failure? || status.error?
193
+ status.failure? || status.error? || status.missing?
192
194
  end
193
195
 
194
196
  def waiting?
@@ -245,7 +247,7 @@ module Shipit
245
247
 
246
248
  def merge_message
247
249
  return title unless merge_requested_by
248
- "#{title}\n\nMerge-Requested-By: #{merge_requested_by.login}\n"
250
+ "#{title}\n\n#{MERGE_REQUEST_FIELD}: #{merge_requested_by.login}\n"
249
251
  end
250
252
 
251
253
  def stale?
@@ -0,0 +1,41 @@
1
+ module Shipit
2
+ class ReleaseStatus < ActiveRecord::Base
3
+ MAX_DESCRIPTION_LENGTH = 140
4
+ include DeferredTouch
5
+
6
+ belongs_to :stack
7
+ belongs_to :commit
8
+ belongs_to :user, optional: true
9
+
10
+ deferred_touch stack: :updated_at, commit: :updated_at
11
+ after_commit :schedule_create_release_statuses, on: :create
12
+
13
+ scope :to_be_created, -> { where(github_id: nil).order(id: :asc) }
14
+
15
+ STATES = %w(pending success failure error).freeze
16
+ validates :state, presence: true, inclusion: {in: STATES}
17
+
18
+ def create_status_on_github!
19
+ return true if github_id?
20
+
21
+ update!(github_id: create_status_on_github.id)
22
+ end
23
+
24
+ private
25
+
26
+ def create_status_on_github
27
+ Shipit.github.api.create_status(
28
+ stack.github_repo_name,
29
+ commit.sha,
30
+ state,
31
+ context: stack.release_status_context,
32
+ target_url: target_url,
33
+ description: description&.truncate(MAX_DESCRIPTION_LENGTH),
34
+ )
35
+ end
36
+
37
+ def schedule_create_release_statuses
38
+ CreateReleaseStatusesJob.perform_later(commit)
39
+ end
40
+ end
41
+ end
@@ -34,6 +34,15 @@ module Shipit
34
34
 
35
35
  private
36
36
 
37
+ def update_release_status
38
+ return unless stack.release_status?
39
+
40
+ case status
41
+ when 'pending'
42
+ deploy.append_release_status('failure', "A rollback #{stack.to_param} was triggered")
43
+ end
44
+ end
45
+
37
46
  def lock_reverted_commits
38
47
  stack.lock_reverted_commits!
39
48
  end
@@ -73,8 +73,8 @@ module Shipit
73
73
  validates :lock_reason, length: {maximum: 4096}
74
74
 
75
75
  serialize :cached_deploy_spec, DeploySpec
76
- delegate :find_task_definition, :supports_rollback?, :links,
77
- :supports_fetch_deployed_revision?, to: :cached_deploy_spec, allow_nil: true
76
+ delegate :find_task_definition, :supports_rollback?, :links, :release_status?, :release_status_delay,
77
+ :release_status_context, :supports_fetch_deployed_revision?, to: :cached_deploy_spec, allow_nil: true
78
78
 
79
79
  def self.refresh_deployed_revisions
80
80
  find_each.select(&:supports_fetch_deployed_revision?).each(&:async_refresh_deployed_revision)
@@ -307,13 +307,8 @@ module Shipit
307
307
  File.join(base_path, "git")
308
308
  end
309
309
 
310
- def acquire_git_cache_lock(timeout: 15, expiration: 60, &block)
311
- Redis::Lock.new(
312
- "stack:#{id}:git-cache-lock",
313
- Shipit.redis,
314
- timeout: timeout,
315
- expiration: expiration,
316
- ).lock(&block)
310
+ def acquire_git_cache_lock(timeout: 15, &block)
311
+ Flock.new(git_path.to_s + '.lock').lock(timeout: timeout, &block)
317
312
  end
318
313
 
319
314
  def clear_git_cache!
@@ -21,6 +21,10 @@ module Shipit
21
21
  state == 'failure'.freeze
22
22
  end
23
23
 
24
+ def missing?
25
+ false
26
+ end
27
+
24
28
  def group?
25
29
  false
26
30
  end
@@ -29,7 +29,8 @@ module Shipit
29
29
  @statuses = visible_statuses.sort_by!(&:context)
30
30
  end
31
31
 
32
- delegate :pending?, :success?, :error?, :failure?, :unknown?, :state, :simple_state, to: :significant_status
32
+ delegate :pending?, :success?, :error?, :failure?, :unknown?, :missing?, :state, :simple_state,
33
+ to: :significant_status
33
34
  delegate :each, :size, :map, to: :statuses
34
35
  delegate :required_statuses, to: :commit
35
36
 
@@ -18,6 +18,10 @@ module Shipit
18
18
  'pending'.freeze
19
19
  end
20
20
 
21
+ def missing?
22
+ true
23
+ end
24
+
21
25
  def description
22
26
  I18n.t('missing_status.description', context: context)
23
27
  end
@@ -1,18 +1,33 @@
1
1
  module Shipit
2
2
  class Status
3
3
  class Unknown
4
+ include GlobalID::Identification
4
5
  include Common
5
6
 
7
+ class << self
8
+ def find(id)
9
+ new(Commit.find(id))
10
+ end
11
+ end
12
+
6
13
  attr_reader :commit
7
14
 
8
15
  def initialize(commit)
9
16
  @commit = commit
10
17
  end
11
18
 
19
+ def id
20
+ commit.id
21
+ end
22
+
12
23
  def state
13
24
  'unknown'.freeze
14
25
  end
15
26
 
27
+ def missing?
28
+ true
29
+ end
30
+
16
31
  def target_url
17
32
  nil
18
33
  end
@@ -289,6 +289,10 @@ module Shipit
289
289
  self.class.name.demodulize.underscore.to_sym
290
290
  end
291
291
 
292
+ def permalink
293
+ Shipit::Engine.routes.url_helpers.stack_task_url(stack, self)
294
+ end
295
+
292
296
  private
293
297
 
294
298
  def prevent_concurrency
@@ -8,6 +8,8 @@ module Shipit
8
8
  has_many :commits, foreign_key: :committer_id, inverse_of: :committer
9
9
  has_many :tasks
10
10
 
11
+ validates :name, presence: true
12
+
11
13
  attr_encrypted :github_access_token, key: Shipit.user_access_tokens_key
12
14
 
13
15
  def self.find_or_create_by_login!(login)
@@ -16,8 +18,19 @@ module Shipit
16
18
  end
17
19
  end
18
20
 
21
+ def self.find_or_create_committer_from_github_commit(github_commit)
22
+ find_or_create_from_github(github_commit.committer.presence || github_commit.commit.committer.presence)
23
+ end
24
+
25
+ def self.find_or_create_author_from_github_commit(github_commit)
26
+ if github_commit.commit.message =~ /^#{PullRequest::MERGE_REQUEST_FIELD}: ([\w\-\.]+)$/
27
+ return find_or_create_by_login!($1)
28
+ end
29
+ find_or_create_from_github(github_commit.author.presence || github_commit.commit.author.presence)
30
+ end
31
+
19
32
  def self.find_or_create_from_github(github_user)
20
- find_from_github(github_user) || create_from_github!(github_user)
33
+ find_from_github(github_user) || create_from_github(github_user)
21
34
  end
22
35
 
23
36
  def self.find_from_github(github_user)
@@ -25,8 +38,8 @@ module Shipit
25
38
  find_by(github_id: github_user.id)
26
39
  end
27
40
 
28
- def self.create_from_github!(github_user)
29
- create!(github_user: github_user)
41
+ def self.create_from_github(github_user)
42
+ create(github_user: github_user)
30
43
  end
31
44
 
32
45
  def self.refresh_shard(shard_index, shards_count)