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.
- checksums.yaml +4 -4
- data/README.md +30 -1
- data/app/assets/stylesheets/_pages/_commits.scss +2 -0
- data/app/assets/stylesheets/_pages/_deploy.scss +6 -0
- data/app/controllers/shipit/api/release_statuses_controller.rb +22 -0
- data/app/controllers/shipit/api/stacks_controller.rb +6 -1
- data/app/controllers/shipit/commits_controller.rb +12 -1
- data/app/controllers/shipit/deploys_controller.rb +11 -0
- data/app/controllers/shipit/stacks_controller.rb +29 -1
- data/app/controllers/shipit/tasks_controller.rb +13 -1
- data/app/helpers/shipit/merge_status_helper.rb +2 -2
- data/app/helpers/shipit/stacks_helper.rb +10 -4
- data/app/jobs/shipit/perform_task_job.rb +1 -0
- data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +41 -0
- data/app/models/shipit/command_line_user.rb +58 -0
- data/app/models/shipit/commit.rb +42 -2
- data/app/models/shipit/deploy.rb +31 -2
- data/app/models/shipit/deploy_spec/lerna_discovery.rb +43 -19
- data/app/models/shipit/deploy_spec/pypi_discovery.rb +5 -1
- data/app/models/shipit/rollback.rb +4 -2
- data/app/models/shipit/stack.rb +72 -15
- data/app/models/shipit/task.rb +30 -0
- data/app/models/shipit/undeployed_commit.rb +10 -1
- data/app/serializers/shipit/command_line_user_serializer.rb +4 -0
- data/app/views/layouts/shipit.html.erb +5 -1
- data/app/views/shipit/commits/_commit.html.erb +7 -2
- data/app/views/shipit/merge_status/_commit_count_warning.html.erb +1 -5
- data/app/views/shipit/merge_status/backlogged.html.erb +1 -1
- data/app/views/shipit/merge_status/failure.html.erb +1 -1
- data/app/views/shipit/merge_status/locked.html.erb +1 -1
- data/app/views/shipit/merge_status/success.html.erb +1 -1
- data/app/views/shipit/stacks/show.html.erb +10 -1
- data/app/views/shipit/tasks/_task_output.html.erb +2 -2
- data/config/locales/en.yml +2 -1
- data/config/routes.rb +8 -1
- data/db/migrate/20190502020249_add_lock_author_id_to_commits.rb +5 -0
- data/lib/shipit.rb +14 -2
- data/lib/shipit/cast_value.rb +9 -0
- data/lib/shipit/command.rb +62 -16
- data/lib/shipit/line_buffer.rb +42 -0
- data/lib/shipit/version.rb +1 -1
- data/lib/tasks/shipit.rake +27 -0
- data/test/controllers/api/release_statuses_controller_test.rb +66 -0
- data/test/controllers/api/stacks_controller_test.rb +19 -0
- data/test/controllers/commits_controller_test.rb +30 -6
- data/test/controllers/deploys_controller_test.rb +51 -2
- data/test/controllers/tasks_controller_test.rb +24 -0
- data/test/dummy/db/schema.rb +2 -1
- data/test/dummy/db/seeds.rb +2 -0
- data/test/fixtures/shipit/check_runs.yml +11 -0
- data/test/fixtures/shipit/commits.yml +104 -0
- data/test/fixtures/shipit/stacks.yml +98 -3
- data/test/fixtures/shipit/tasks.yml +42 -0
- data/test/jobs/update_github_last_deployed_ref_job_test.rb +88 -0
- data/test/models/commits_test.rb +88 -1
- data/test/models/deploy_spec_test.rb +34 -6
- data/test/models/deploys_test.rb +308 -6
- data/test/models/rollbacks_test.rb +17 -11
- data/test/models/stacks_test.rb +217 -4
- data/test/models/tasks_test.rb +13 -0
- data/test/models/undeployed_commits_test.rb +62 -3
- data/test/test_helper.rb +0 -1
- data/test/unit/command_test.rb +55 -0
- data/test/unit/line_buffer_test.rb +20 -0
- 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.
|
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
|
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
|
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
|
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
|
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: @
|
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
|
35
|
-
<span class="caption--pending">Aborting with Rollback
|
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>
|
data/config/locales/en.yml
CHANGED
@@ -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
|
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
|
data/config/routes.rb
CHANGED
@@ -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}
|
data/lib/shipit.rb
CHANGED
@@ -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(
|
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
|
data/lib/shipit/command.rb
CHANGED
@@ -23,13 +23,14 @@ module Shipit
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
attr_reader :out, :
|
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}
|
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
|
-
|
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
|
187
|
-
return
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
data/lib/shipit/version.rb
CHANGED
@@ -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
|