shipit-engine 0.24.0 → 0.25.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 (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)