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.
- 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
|