shipit-engine 0.27.1 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -1
  3. data/app/assets/stylesheets/_pages/_commits.scss +2 -0
  4. data/app/assets/stylesheets/_pages/_deploy.scss +6 -0
  5. data/app/controllers/shipit/api/release_statuses_controller.rb +22 -0
  6. data/app/controllers/shipit/api/stacks_controller.rb +6 -1
  7. data/app/controllers/shipit/commits_controller.rb +12 -1
  8. data/app/controllers/shipit/deploys_controller.rb +11 -0
  9. data/app/controllers/shipit/stacks_controller.rb +29 -1
  10. data/app/controllers/shipit/tasks_controller.rb +13 -1
  11. data/app/helpers/shipit/merge_status_helper.rb +2 -2
  12. data/app/helpers/shipit/stacks_helper.rb +10 -4
  13. data/app/jobs/shipit/perform_task_job.rb +1 -0
  14. data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +41 -0
  15. data/app/models/shipit/command_line_user.rb +58 -0
  16. data/app/models/shipit/commit.rb +42 -2
  17. data/app/models/shipit/deploy.rb +31 -2
  18. data/app/models/shipit/deploy_spec/lerna_discovery.rb +43 -19
  19. data/app/models/shipit/deploy_spec/pypi_discovery.rb +5 -1
  20. data/app/models/shipit/rollback.rb +4 -2
  21. data/app/models/shipit/stack.rb +72 -15
  22. data/app/models/shipit/task.rb +30 -0
  23. data/app/models/shipit/undeployed_commit.rb +10 -1
  24. data/app/serializers/shipit/command_line_user_serializer.rb +4 -0
  25. data/app/views/layouts/shipit.html.erb +5 -1
  26. data/app/views/shipit/commits/_commit.html.erb +7 -2
  27. data/app/views/shipit/merge_status/_commit_count_warning.html.erb +1 -5
  28. data/app/views/shipit/merge_status/backlogged.html.erb +1 -1
  29. data/app/views/shipit/merge_status/failure.html.erb +1 -1
  30. data/app/views/shipit/merge_status/locked.html.erb +1 -1
  31. data/app/views/shipit/merge_status/success.html.erb +1 -1
  32. data/app/views/shipit/stacks/show.html.erb +10 -1
  33. data/app/views/shipit/tasks/_task_output.html.erb +2 -2
  34. data/config/locales/en.yml +2 -1
  35. data/config/routes.rb +8 -1
  36. data/db/migrate/20190502020249_add_lock_author_id_to_commits.rb +5 -0
  37. data/lib/shipit.rb +14 -2
  38. data/lib/shipit/cast_value.rb +9 -0
  39. data/lib/shipit/command.rb +62 -16
  40. data/lib/shipit/line_buffer.rb +42 -0
  41. data/lib/shipit/version.rb +1 -1
  42. data/lib/tasks/shipit.rake +27 -0
  43. data/test/controllers/api/release_statuses_controller_test.rb +66 -0
  44. data/test/controllers/api/stacks_controller_test.rb +19 -0
  45. data/test/controllers/commits_controller_test.rb +30 -6
  46. data/test/controllers/deploys_controller_test.rb +51 -2
  47. data/test/controllers/tasks_controller_test.rb +24 -0
  48. data/test/dummy/db/schema.rb +2 -1
  49. data/test/dummy/db/seeds.rb +2 -0
  50. data/test/fixtures/shipit/check_runs.yml +11 -0
  51. data/test/fixtures/shipit/commits.yml +104 -0
  52. data/test/fixtures/shipit/stacks.yml +98 -3
  53. data/test/fixtures/shipit/tasks.yml +42 -0
  54. data/test/jobs/update_github_last_deployed_ref_job_test.rb +88 -0
  55. data/test/models/commits_test.rb +88 -1
  56. data/test/models/deploy_spec_test.rb +34 -6
  57. data/test/models/deploys_test.rb +308 -6
  58. data/test/models/rollbacks_test.rb +17 -11
  59. data/test/models/stacks_test.rb +217 -4
  60. data/test/models/tasks_test.rb +13 -0
  61. data/test/models/undeployed_commits_test.rb +62 -3
  62. data/test/test_helper.rb +0 -1
  63. data/test/unit/command_test.rb +55 -0
  64. data/test/unit/line_buffer_test.rb +20 -0
  65. metadata +142 -128
@@ -1,8 +1,4 @@
1
1
  <p class="status-heading text-red">
2
2
  <%= render 'warning_icon' %>
3
- Consider rebasing into a smaller number of commits containing atomic changes. Find out
4
- <%= link_to 'why', 'https://docs.google.com/document/d/1lZlNo7ekugQDxug6Nc6pdY87VNobl3g2IQjY7R0YIIc',
5
- target: '_blank',
6
- rel: 'noopener'
7
- %>.
3
+ Consider rebasing into a smaller number of commits containing atomic changes.
8
4
  </p>
@@ -17,5 +17,5 @@
17
17
  <%= link_to @stack.to_param, stack_url(@stack), target: '_blank', rel: 'noopener' %>
18
18
  is <strong>backlogged</strong>
19
19
  </span>
20
- <%= render 'commit_count_warning' if too_many_commits?(params[:commits].to_i) %>
20
+ <%= render 'commit_count_warning' if display_commit_count_warning?(params[:commits].to_i) %>
21
21
  </div>
@@ -17,5 +17,5 @@
17
17
  <%= link_to @stack.to_param, stack_url(@stack), target: '_blank', rel: 'noopener' %>
18
18
  <strong>master branch is failing!</strong>
19
19
  </span>
20
- <%= render 'commit_count_warning' if too_many_commits?(params[:commits].to_i) %>
20
+ <%= render 'commit_count_warning' if display_commit_count_warning?(params[:commits].to_i) %>
21
21
  </div>
@@ -17,5 +17,5 @@
17
17
  <%= link_to @stack.to_param, stack_url(@stack), target: '_blank', rel: 'noopener' %>
18
18
  is <strong>locked</strong> because: <strong><%= auto_link(emojify(@stack.lock_reason), html: { target: '_blank' }) %></strong>
19
19
  </span>
20
- <%= render 'commit_count_warning' if too_many_commits?(params[:commits].to_i) %>
20
+ <%= render 'commit_count_warning' if display_commit_count_warning?(params[:commits].to_i) %>
21
21
  </div>
@@ -20,5 +20,5 @@
20
20
  <%= link_to @stack.to_param, stack_url(@stack), target: '_blank', rel: 'noopener' %> is clear.
21
21
  </span>
22
22
 
23
- <%= render 'commit_count_warning' if too_many_commits?(params[:commits].to_i) %>
23
+ <%= render 'commit_count_warning' if display_commit_count_warning?(params[:commits].to_i) %>
24
24
  </div>
@@ -17,7 +17,16 @@
17
17
  </div>
18
18
  </header>
19
19
  <ul class="commit-list">
20
- <%= render partial: 'shipit/commits/commit', collection: @commits %>
20
+ <%= render partial: 'shipit/commits/commit', collection: @undeployed_commits %>
21
+ </ul>
22
+ </section>
23
+
24
+ <section>
25
+ <header class="section-header">
26
+ <h2>Currently Deploying Commits</h2>
27
+ </header>
28
+ <ul class="commit-list">
29
+ <%= render partial: 'shipit/commits/commit', collection: @active_commits %>
21
30
  </ul>
22
31
  </section>
23
32
 
@@ -31,8 +31,8 @@
31
31
 
32
32
  <% if task.supports_rollback? %>
33
33
  <%= link_to abort_stack_task_path(@stack, task, rollback: true), class: "btn btn--delete action-button", data: { action: "abort", rollback: true, status: task.status } do %>
34
- <span class="caption--ready">Abort and Rollback</span>
35
- <span class="caption--pending">Aborting with Rollback...</span>
34
+ <span class="caption--ready">Abort and Rollback to <span class="short-sha-no-bg"><%= short_commit_sha(task) %></span></span>
35
+ <span class="caption--pending">Aborting with Rollback... to <span class="short-sha-no-bg"><%= short_commit_sha(task) %></span></span>
36
36
  <% end %>
37
37
  <% end %>
38
38
  </div>
@@ -22,7 +22,8 @@
22
22
  en:
23
23
  commit:
24
24
  lock: This commit is safe to deploy. Click to mark it as unsafe.
25
- unlock: This commit is unsafe to deploy. Click to mark it as safe.
25
+ unlock: This commit was marked as unsafe to deploy. Click to mark it as safe.
26
+ unlock_with_author: "%{author} marked this commit as unsafe to deploy. Click to mark it as safe."
26
27
  confirm_unlock: Mark this commit as safe to deploy?
27
28
  release:
28
29
  validate: Mark the release as healthy
@@ -16,6 +16,7 @@ Shipit::Engine.routes.draw do
16
16
  resources :stacks, only: %i(index create)
17
17
  scope '/stacks/*id', id: stack_id_format, as: :stack do
18
18
  get '/' => 'stacks#show'
19
+ delete '/' => 'stacks#destroy'
19
20
  end
20
21
 
21
22
  scope '/stacks/*stack_id', stack_id: stack_id_format, as: :stack do
@@ -24,7 +25,9 @@ Shipit::Engine.routes.draw do
24
25
  resources :tasks, only: %i(index show) do
25
26
  resource :output, only: :show
26
27
  end
27
- resources :deploys, only: %i(index create)
28
+ resources :deploys, only: %i(index create) do
29
+ resources :release_statuses, only: %i(create)
30
+ end
28
31
  resources :commits, only: %i(index)
29
32
  resources :pull_requests, only: %i(index show update destroy)
30
33
  post '/task/:task_name' => 'tasks#trigger', as: :trigger_task
@@ -63,6 +66,10 @@ Shipit::Engine.routes.draw do
63
66
  post :clear_git_cache, controller: :stacks
64
67
  end
65
68
 
69
+ scope '/task/:id', controller: :tasks do
70
+ get '/', action: :lookup
71
+ end
72
+
66
73
  scope '/*stack_id', stack_id: stack_id_format, as: :stack do
67
74
  get '/commit/:sha/checks' => 'commit_checks#show', as: :commit_checks
68
75
  get '/commit/:sha/checks/tail' => 'commit_checks#tail', as: :tail_commit_checks, defaults: {format: :json}
@@ -0,0 +1,5 @@
1
+ class AddLockAuthorIdToCommits < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column(:commits, :lock_author_id, :integer, limit: 4)
4
+ end
5
+ end
@@ -48,6 +48,8 @@ require 'shipit/rollback_commands'
48
48
  require 'shipit/environment_variables'
49
49
  require 'shipit/stat'
50
50
  require 'shipit/strip_cache_control'
51
+ require 'shipit/cast_value'
52
+ require 'shipit/line_buffer'
51
53
 
52
54
  SafeYAML::OPTIONS[:default_mode] = :safe
53
55
  SafeYAML::OPTIONS[:deserialize_symbols] = false
@@ -58,7 +60,7 @@ module Shipit
58
60
  delegate :table_name_prefix, to: :secrets
59
61
 
60
62
  attr_accessor :disable_api_authentication, :timeout_exit_codes
61
- attr_writer :internal_hook_receivers
63
+ attr_writer :internal_hook_receivers, :task_logger
62
64
 
63
65
  self.timeout_exit_codes = [].freeze
64
66
 
@@ -75,7 +77,13 @@ module Shipit
75
77
  end
76
78
 
77
79
  def redis(namespace = nil)
78
- @redis ||= Redis.new(url: redis_url.to_s.presence, logger: Rails.logger)
80
+ @redis ||= Redis.new(
81
+ url: redis_url.to_s.presence,
82
+ logger: Rails.logger,
83
+ reconnect_attempts: 3,
84
+ reconnect_delay: 0.5,
85
+ reconnect_delay_max: 1,
86
+ )
79
87
  return @redis unless namespace
80
88
  Redis::Namespace.new(namespace, redis: @redis)
81
89
  end
@@ -188,6 +196,10 @@ module Shipit
188
196
  @internal_hook_receivers ||= []
189
197
  end
190
198
 
199
+ def task_logger
200
+ @task_logger ||= Logger.new(nil)
201
+ end
202
+
191
203
  protected
192
204
 
193
205
  def revision_file
@@ -0,0 +1,9 @@
1
+ module Shipit
2
+ module CastValue
3
+ def to_boolean(value)
4
+ ActiveModel::Type::Boolean.new.serialize(value)
5
+ end
6
+
7
+ module_function :to_boolean
8
+ end
9
+ end
@@ -23,13 +23,14 @@ module Shipit
23
23
  end
24
24
  end
25
25
 
26
- attr_reader :out, :code, :chdir, :env, :args, :pid, :timeout
26
+ attr_reader :out, :chdir, :env, :args, :pid, :timeout
27
27
 
28
28
  def initialize(*args, default_timeout: Shipit.default_inactivity_timeout, env: {}, chdir:)
29
29
  @args, options = parse_arguments(args)
30
30
  @timeout = options['timeout'.freeze] || options[:timeout] || default_timeout
31
31
  @env = env
32
32
  @chdir = chdir.to_s
33
+ @timed_out = false
33
34
  end
34
35
 
35
36
  def with_timeout(new_timeout)
@@ -58,7 +59,7 @@ module Shipit
58
59
  end
59
60
 
60
61
  def exit_message
61
- "#{self} exited with status #{@code}"
62
+ "#{self} #{termination_status}"
62
63
  end
63
64
 
64
65
  def run
@@ -107,16 +108,15 @@ module Shipit
107
108
  begin
108
109
  read_stream(@out, &block)
109
110
  rescue TimedOut => error
110
- @code = 'timeout'
111
111
  yield red("No output received in the last #{timeout} seconds.") + "\n"
112
112
  terminate!(&block)
113
113
  raise error
114
114
  rescue Errno::EIO # Somewhat expected on Linux: http://stackoverflow.com/a/10306782
115
115
  end
116
116
 
117
- _, status = Process.waitpid2(@pid)
118
- @code = status.exitstatus
119
117
  self
118
+ ensure
119
+ reap_child!
120
120
  end
121
121
 
122
122
  def red(text)
@@ -130,6 +130,10 @@ module Shipit
130
130
  end
131
131
 
132
132
  def timed_out?
133
+ @timed_out
134
+ end
135
+
136
+ def output_timed_out?
133
137
  @last_output_at ||= Time.now.to_i
134
138
  (@last_output_at + timeout) < Time.now.to_i
135
139
  end
@@ -150,7 +154,10 @@ module Shipit
150
154
  yield io.read_nonblock(MAX_READ)
151
155
  touch_last_output_at
152
156
  rescue IO::WaitReadable
153
- raise TimedOut if timed_out?
157
+ if output_timed_out?
158
+ @timed_out = true
159
+ raise TimedOut
160
+ end
154
161
  IO.select([io], nil, nil, 1)
155
162
  retry
156
163
  end
@@ -183,18 +190,18 @@ module Shipit
183
190
  rescue TimedOut
184
191
  rescue Errno::EIO # EIO is somewhat expected on Linux: http://stackoverflow.com/a/10306782
185
192
  # If we try to read the stream right after sending a signal, we often get an Errno::EIO.
186
- if status = Process.wait(@pid, Process::WNOHANG)
187
- return status
188
- else
189
- # If we let the child a little bit of time, it solves it.
190
- retry_count -= 1
191
- if retry_count > 0
192
- sleep 0.05
193
- retry
194
- end
193
+ if reap_child!(block: false)
194
+ return true
195
+ end
196
+ # If we let the child a little bit of time, it solves it.
197
+ retry_count -= 1
198
+ if retry_count > 0
199
+ sleep 0.05
200
+ retry
195
201
  end
196
202
  end
197
- Process.wait(@pid, Process::WNOHANG)
203
+ reap_child!(block: false)
204
+ true
198
205
  end
199
206
 
200
207
  def kill(sig)
@@ -215,5 +222,44 @@ module Shipit
215
222
  end
216
223
  return args, options
217
224
  end
225
+
226
+ def running?
227
+ !!pid && !@status
228
+ end
229
+
230
+ def code
231
+ @status&.exitstatus
232
+ end
233
+
234
+ def signaled?
235
+ @status.signaled?
236
+ end
237
+
238
+ def reap_child!(block: true)
239
+ return @status if @status
240
+ return unless running? # Command was never started e.g. permission denied, not found etc
241
+ if block
242
+ _, @status = Process.waitpid2(@pid)
243
+ elsif res = Process.waitpid2(@pid, Process::WNOHANG)
244
+ @status = res[1]
245
+ end
246
+ @status
247
+ end
248
+
249
+ def termination_status
250
+ if running?
251
+ "is running"
252
+ elsif success?
253
+ "terminated successfully"
254
+ elsif timed_out? && signaled?
255
+ "timed out and terminated with #{Signal.signame(@status.termsig)} signal"
256
+ elsif timed_out?
257
+ "timed out and terminated with exit status #{exitstatus}"
258
+ elsif signaled?
259
+ "terminated with #{Signal.signame(@status.termsig)} signal"
260
+ else
261
+ "terminated with exit status #{code}"
262
+ end
263
+ end
218
264
  end
219
265
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shipit
4
+ class LineBuffer
5
+ SEPARATOR = "\n"
6
+
7
+ def initialize(queue = "")
8
+ @queue = queue.dup
9
+ end
10
+
11
+ def buffer(text, &block)
12
+ @queue << text
13
+ whole_lines.each(&block).tap { flush }
14
+ end
15
+
16
+ def empty?
17
+ @queue.empty?
18
+ end
19
+
20
+ private
21
+
22
+ def whole_lines
23
+ whole? ? lines : lines[0..-2]
24
+ end
25
+
26
+ def flush
27
+ whole? ? clear : @queue = lines.last
28
+ end
29
+
30
+ def whole?
31
+ @queue.end_with?(SEPARATOR)
32
+ end
33
+
34
+ def lines
35
+ @queue.split(SEPARATOR)
36
+ end
37
+
38
+ def clear
39
+ @queue.clear
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module Shipit
2
- VERSION = '0.27.1'.freeze
2
+ VERSION = '0.28.0'.freeze
3
3
  end
@@ -0,0 +1,27 @@
1
+ namespace :shipit do
2
+ desc "Deploy from a running instance. "
3
+ task deploy: :environment do
4
+ begin
5
+ stack = ENV['stack']
6
+ revision = ENV['revision']
7
+
8
+ raise ArgumentError.new('The first argument has to be a stack, e.g. shopify/shipit/production') if stack.nil?
9
+ raise ArgumentError.new('The second argument has to be a revision') if revision.nil?
10
+
11
+ module Shipit
12
+ class Task
13
+ def write(text)
14
+ p text
15
+ chunks.create!(text: text)
16
+ end
17
+ end
18
+ end
19
+
20
+ Shipit::Stack.run_deploy_in_foreground(stack: stack, revision: revision)
21
+ rescue ArgumentError
22
+ p "Use this command as follows:"
23
+ p "bundle exec rake shipit:deploy stack='shopify/shipit/production' revision='$SHA'"
24
+ raise
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ require 'test_helper'
2
+
3
+ module Shipit
4
+ module Api
5
+ class ReleaseStatusesControllerTest < ActionController::TestCase
6
+ setup do
7
+ authenticate!
8
+ @stack = shipit_stacks(:shipit_canaries)
9
+ @deploy = shipit_deploys(:canaries_validating)
10
+ end
11
+
12
+ test "#create renders a 422 if status is not found" do
13
+ post :create, params: {stack_id: @stack.to_param, deploy_id: @deploy.id}
14
+ assert_response :unprocessable_entity
15
+ assert_json 'errors', 'status' => ['is required', 'is not included in the list']
16
+ end
17
+
18
+ test "#create renders a 422 if status is invalid" do
19
+ assert_no_difference -> { ReleaseStatus.count } do
20
+ post :create, params: {
21
+ stack_id: @stack.to_param,
22
+ deploy_id: @deploy.id,
23
+ status: 'foo',
24
+ }
25
+ end
26
+
27
+ assert_response :unprocessable_entity
28
+ assert_json 'errors', 'status' => ['is not included in the list']
29
+ end
30
+
31
+ test "#create allow users to append release statuses and mark the deploy as success" do
32
+ assert_difference -> { ReleaseStatus.count }, +1 do
33
+ post :create, params: {
34
+ stack_id: @stack.to_param,
35
+ deploy_id: @deploy.id,
36
+ status: 'success',
37
+ }
38
+ assert_response :created
39
+ end
40
+
41
+ status = ReleaseStatus.last
42
+ assert_equal 'success', status.state
43
+ assert_equal '@anonymous signaled this release as healthy.', status.description
44
+ assert_equal @deploy.permalink, status.target_url
45
+ assert_equal 'success', @deploy.reload.status
46
+ end
47
+
48
+ test "#create allow users to append release statuses and mark the deploy as faulty" do
49
+ assert_difference -> { ReleaseStatus.count }, +1 do
50
+ post :create, params: {
51
+ stack_id: @stack.to_param,
52
+ deploy_id: @deploy.id,
53
+ status: 'failure',
54
+ }
55
+ assert_response :created
56
+ end
57
+
58
+ status = ReleaseStatus.last
59
+ assert_equal 'failure', status.state
60
+ assert_equal '@anonymous signaled this release as faulty.', status.description
61
+ assert_equal @deploy.permalink, status.target_url
62
+ assert_equal 'faulty', @deploy.reload.status
63
+ end
64
+ end
65
+ end
66
+ end