shipit-engine 0.27.1 → 0.28.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 (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