shipit-engine 0.35.1 → 0.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -7
  3. data/app/controllers/concerns/shipit/authentication.rb +5 -1
  4. data/app/controllers/shipit/api/base_controller.rb +13 -1
  5. data/app/controllers/shipit/api/rollbacks_controller.rb +1 -1
  6. data/app/controllers/shipit/api/stacks_controller.rb +10 -2
  7. data/app/controllers/shipit/api/tasks_controller.rb +19 -2
  8. data/app/controllers/shipit/rollbacks_controller.rb +5 -1
  9. data/app/helpers/shipit/stacks_helper.rb +11 -0
  10. data/app/models/concerns/shipit/deferred_touch.rb +3 -3
  11. data/app/models/shipit/anonymous_user.rb +4 -0
  12. data/app/models/shipit/api_client.rb +1 -1
  13. data/app/models/shipit/commit_checks.rb +3 -3
  14. data/app/models/shipit/delivery.rb +1 -1
  15. data/app/models/shipit/deploy.rb +1 -0
  16. data/app/models/shipit/deploy_spec/file_system.rb +32 -4
  17. data/app/models/shipit/pull_request.rb +1 -1
  18. data/app/models/shipit/stack.rb +10 -10
  19. data/app/models/shipit/task.rb +31 -4
  20. data/app/models/shipit/user.rb +23 -9
  21. data/app/serializers/shipit/stack_serializer.rb +1 -1
  22. data/app/views/shipit/deploys/_deploy.html.erb +1 -5
  23. data/app/views/shipit/stacks/_banners.html.erb +1 -1
  24. data/app/views/shipit/stacks/_settings_form.erb +55 -0
  25. data/app/views/shipit/stacks/settings.html.erb +1 -55
  26. data/app/views/shipit/stacks/show.html.erb +1 -1
  27. data/config/locales/en.yml +1 -1
  28. data/config/routes.rb +4 -0
  29. data/db/migrate/20211103154121_increase_github_team_slug_size.rb +5 -0
  30. data/lib/shipit/engine.rb +15 -5
  31. data/lib/shipit/stack_commands.rb +9 -2
  32. data/lib/shipit/task_commands.rb +8 -1
  33. data/lib/shipit/version.rb +1 -1
  34. data/lib/shipit.rb +55 -3
  35. data/lib/snippets/fetch-gem-version +1 -1
  36. data/test/controllers/api/hooks_controller_test.rb +1 -1
  37. data/test/controllers/api/rollback_controller_test.rb +1 -0
  38. data/test/controllers/api/stacks_controller_test.rb +34 -0
  39. data/test/controllers/api/tasks_controller_test.rb +56 -0
  40. data/test/controllers/stacks_controller_test.rb +11 -0
  41. data/test/dummy/config/application.rb +1 -2
  42. data/test/dummy/db/schema.rb +2 -2
  43. data/test/fixtures/shipit/check_runs.yml +3 -3
  44. data/test/fixtures/shipit/commits.yml +101 -101
  45. data/test/fixtures/shipit/deliveries.yml +1 -1
  46. data/test/fixtures/shipit/merge_requests.yml +19 -19
  47. data/test/fixtures/shipit/stacks.yml +28 -28
  48. data/test/fixtures/shipit/statuses.yml +16 -16
  49. data/test/fixtures/shipit/tasks.yml +77 -65
  50. data/test/fixtures/shipit/users.yml +2 -5
  51. data/test/models/commits_test.rb +6 -6
  52. data/test/models/deploy_spec_test.rb +0 -23
  53. data/test/models/deploys_test.rb +26 -0
  54. data/test/models/shipit/deploy_spec/file_system_test.rb +81 -0
  55. data/test/models/tasks_test.rb +14 -2
  56. data/test/models/team_test.rb +21 -2
  57. data/test/models/users_test.rb +29 -9
  58. data/test/unit/deploy_commands_test.rb +6 -2
  59. metadata +189 -185
@@ -6,61 +6,7 @@
6
6
  <h2>Settings (Stack #<%= @stack.id %>)</h2>
7
7
  </header>
8
8
 
9
- <div class="setting-section">
10
- <%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
11
- <div class="field-wrapper">
12
- <%= f.label :environment %>
13
- <%= f.text_field :environment, placeholder: 'production' %>
14
- </div>
15
-
16
- <div class="field-wrapper">
17
- <span>Branch: <%= @stack.branch %></span>
18
- </div>
19
-
20
- <div class="field-wrapper">
21
- <%= f.label :deploy_url, 'Deploy URL (Where is this stack deployed to?)' %>
22
- <%= f.text_field :deploy_url, placeholder: 'https://' %>
23
- </div>
24
-
25
- <div class="field-wrapper">
26
- <%= f.check_box :continuous_deployment %>
27
- <%= f.label :continuous_deployment, 'Enable continuous deployment' %>
28
- </div>
29
-
30
- <div class="field-wrapper">
31
- <%= f.check_box :merge_queue_enabled %>
32
- <%= f.label :merge_queue_enabled, 'Enable merge queue' %>
33
- </div>
34
-
35
- <div class="field-wrapper">
36
- <%= f.check_box :ignore_ci %>
37
- <%= f.label :ignore_ci, "Don't require CI to deploy" %>
38
- </div>
39
-
40
- <%= f.submit class: "btn", value: "Save" %>
41
- <% end %>
42
- </div>
43
-
44
- <div class="setting-section">
45
- <h5>Lock deploys</h5>
46
- <%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
47
- <div class="field-wrapper">
48
- <%= f.label :lock_reason, 'Reason for lock' %>
49
- <%= f.text_area :lock_reason %>
50
- </div>
51
- <% if @stack.locked? %>
52
- <%= f.submit class: "btn", value: "Update Reason" %>
53
- <% else %>
54
- <%= f.submit class: "btn", value: "Lock" %>
55
- <% end %>
56
- <% end %>
57
- <% if @stack.locked? %>
58
- <%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
59
- <%= f.hidden_field :lock_reason, value: nil %>
60
- <%= f.submit class: "btn btn--primary", value: "Unlock" %>
61
- <%- end -%>
62
- <% end %>
63
- </div>
9
+ <%= render partial: 'shipit/stacks/settings_form', locals: { stack: @stack } %>
64
10
 
65
11
  <div class="setting-section">
66
12
  <h5>Resynchronize this stack</h5>
@@ -30,7 +30,7 @@
30
30
  </ul>
31
31
  </section>
32
32
 
33
- <% cache @stack do %>
33
+ <% cache [@stack, params[:force]] do %>
34
34
  <section>
35
35
  <header class="section-header">
36
36
  <h2>Previous Deploys</h2>
@@ -41,7 +41,7 @@ en:
41
41
  reject: Mark the release as faulty
42
42
  deploy_button:
43
43
  hint:
44
- max_commits: It is recommended not to deploy more than %{maximum} commits at once.
44
+ max_commits: Use caution when deploying more than %{maximum} commits at once.
45
45
  blocked: This commit range includes a commit that can't be deployed.
46
46
  caption:
47
47
  pending: Pending CI
data/config/routes.rb CHANGED
@@ -20,6 +20,7 @@ Shipit::Engine.routes.draw do
20
20
  get '/' => 'stacks#show'
21
21
  delete '/' => 'stacks#destroy'
22
22
  patch '/' => 'stacks#update'
23
+ post '/refresh' => 'stacks#refresh'
23
24
  end
24
25
 
25
26
  scope '/stacks/*stack_id', stack_id: stack_id_format, as: :stack do
@@ -27,6 +28,9 @@ Shipit::Engine.routes.draw do
27
28
  resource :lock, only: %i(create update destroy)
28
29
  resources :tasks, only: %i(index show) do
29
30
  resource :output, only: :show
31
+ member do
32
+ put :abort
33
+ end
30
34
  end
31
35
  resources :deploys, only: %i(index create) do
32
36
  resources :release_statuses, only: %i(create)
@@ -0,0 +1,5 @@
1
+ class IncreaseGithubTeamSlugSize < ActiveRecord::Migration[6.1]
2
+ def change
3
+ change_column :teams, :slug, :string, limit: 255, null: true
4
+ end
5
+ end
data/lib/shipit/engine.rb CHANGED
@@ -5,6 +5,17 @@ module Shipit
5
5
 
6
6
  paths['app/models'] << 'app/serializers' << 'app/serializers/concerns'
7
7
 
8
+ initializer 'shipit.encryption_config', before: 'active_record_encryption.configuration' do |app|
9
+ if app.credentials.active_record_encryption.blank? && Shipit.user_access_tokens_key.present?
10
+ # For ease of upgrade, we derive an Active Record encryption config automatically.
11
+ # But if AR Encryption is already configured, we just use that
12
+ app.credentials[:active_record_encryption] = {
13
+ primary_key: Shipit.user_access_tokens_key,
14
+ key_derivation_salt: Digest::SHA256.digest("salt:".b + Shipit.user_access_tokens_key),
15
+ }
16
+ end
17
+ end
18
+
8
19
  initializer 'shipit.config' do |app|
9
20
  Rails.application.routes.default_url_options[:host] = Shipit.host
10
21
  Shipit::Engine.routes.default_url_options[:host] = Shipit.host
@@ -28,8 +39,6 @@ module Shipit
28
39
  path.end_with?('.svg') || (path.start_with?('emoji/') && path.end_with?('.png'))
29
40
  end
30
41
 
31
- ActionDispatch::ExceptionWrapper.rescue_responses[Shipit::TaskDefinition::NotFound.name] = :not_found
32
-
33
42
  ActiveModel::Serializer._root = false
34
43
  ActiveModel::ArraySerializer._root = false
35
44
  ActiveModel::Serializer.include(Engine.routes.url_helpers)
@@ -44,10 +53,11 @@ module Shipit
44
53
  if Shipit.enable_samesite_middleware?
45
54
  app.config.middleware.insert_after(::Rack::Runtime, Shipit::SameSiteCookieMiddleware)
46
55
  end
56
+ end
47
57
 
48
- app.config.after_initialize do
49
- ActionController::Base.include(Shipit::ActiveModelSerializersPatch)
50
- end
58
+ config.after_initialize do
59
+ ActionDispatch::ExceptionWrapper.rescue_responses[Shipit::TaskDefinition::NotFound.name] = :not_found
60
+ ActionController::Base.include(Shipit::ActiveModelSerializersPatch)
51
61
  end
52
62
  end
53
63
  end
@@ -16,7 +16,7 @@ module Shipit
16
16
  def fetch
17
17
  create_directories
18
18
  if valid_git_repository?(@stack.git_path)
19
- git('fetch', 'origin', '--tags', @stack.branch, env: env, chdir: @stack.git_path)
19
+ git('fetch', 'origin', '--quiet', '--tags', @stack.branch, env: env, chdir: @stack.git_path)
20
20
  else
21
21
  @stack.clear_git_cache!
22
22
  git_clone(@stack.repo_git_url, @stack.git_path, branch: @stack.branch, env: env, chdir: @stack.deploys_path)
@@ -72,7 +72,14 @@ module Shipit
72
72
  ).run!
73
73
 
74
74
  git_dir = File.join(dir, @stack.repo_name)
75
- git('-c', 'advice.detachedHead=false', 'checkout', commit.sha, chdir: git_dir).run! if commit
75
+ git(
76
+ '-c',
77
+ 'advice.detachedHead=false',
78
+ 'checkout',
79
+ '--quiet',
80
+ commit.sha,
81
+ chdir: git_dir
82
+ ).run! if commit
76
83
  yield Pathname.new(git_dir)
77
84
  end
78
85
  end
@@ -47,7 +47,14 @@ module Shipit
47
47
  end
48
48
 
49
49
  def checkout(commit)
50
- git('-c', 'advice.detachedHead=false', 'checkout', commit.sha, chdir: @task.working_directory)
50
+ git(
51
+ '-c',
52
+ 'advice.detachedHead=false',
53
+ 'checkout',
54
+ '--quiet',
55
+ commit.sha,
56
+ chdir: @task.working_directory
57
+ )
51
58
  end
52
59
 
53
60
  def clone
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Shipit
3
- VERSION = '0.35.1'
3
+ VERSION = '0.37.0'
4
4
  end
data/lib/shipit.rb CHANGED
@@ -5,7 +5,7 @@ require 'state_machines-activerecord'
5
5
  require 'validate_url'
6
6
  require 'responders'
7
7
  require 'explicit-parameters'
8
- require 'attr_encrypted'
8
+ require 'paquito'
9
9
 
10
10
  require 'sass-rails'
11
11
  require 'coffee-rails'
@@ -64,7 +64,8 @@ module Shipit
64
64
 
65
65
  delegate :table_name_prefix, to: :secrets
66
66
 
67
- attr_accessor :disable_api_authentication, :timeout_exit_codes, :deployment_checks
67
+ attr_accessor :disable_api_authentication, :timeout_exit_codes, :deployment_checks, :respect_bare_shipit_file,
68
+ :database_serializer
68
69
  attr_writer(
69
70
  :internal_hook_receivers,
70
71
  :preferred_org_emails,
@@ -77,6 +78,9 @@ module Shipit
77
78
  end
78
79
 
79
80
  self.timeout_exit_codes = [].freeze
81
+ self.respect_bare_shipit_file = true
82
+
83
+ alias_method :respect_bare_shipit_file?, :respect_bare_shipit_file
80
84
 
81
85
  def authentication_disabled?
82
86
  ENV['SHIPIT_DISABLE_AUTH'].present?
@@ -104,6 +108,50 @@ module Shipit
104
108
  )
105
109
  end
106
110
 
111
+ module SafeJSON
112
+ class << self
113
+ def load(serial)
114
+ return nil if serial.nil?
115
+ # JSON.load is unsafe, we should use parse instead
116
+ JSON.parse(serial)
117
+ end
118
+
119
+ def dump(object)
120
+ JSON.dump(object)
121
+ end
122
+ end
123
+ end
124
+
125
+ module TransitionalSerializer
126
+ SafeYAML = Paquito::SafeYAML.new(deprecated_classes: ["ActiveSupport::HashWithIndifferentAccess"])
127
+
128
+ class << self
129
+ def load(serial)
130
+ return if serial.nil?
131
+
132
+ JSON.parse(serial)
133
+ rescue JSON::ParserError
134
+ SafeYAML.load(serial)
135
+ end
136
+
137
+ def dump(object)
138
+ return if object.nil?
139
+ JSON.dump(object)
140
+ end
141
+ end
142
+ end
143
+
144
+ self.database_serializer = TransitionalSerializer
145
+
146
+ def serialized_column(attribute_name, type: nil, coder: nil)
147
+ column = Paquito::SerializedColumn.new(database_serializer, type, attribute_name: attribute_name)
148
+ if coder
149
+ Paquito.chain(coder, column)
150
+ else
151
+ column
152
+ end
153
+ end
154
+
107
155
  def github(organization: github_default_organization)
108
156
  # Backward compatibility
109
157
  # nil signifies the single github app config schema is being used
@@ -153,7 +201,11 @@ module Shipit
153
201
  end
154
202
 
155
203
  def user_access_tokens_key
156
- (secrets.user_access_tokens_key.presence || secrets.secret_key_base).byteslice(0, 32)
204
+ if secrets.user_access_tokens_key.present?
205
+ secrets.user_access_tokens_key
206
+ elsif secrets.secret_key_base
207
+ Digest::SHA256.digest("user_access_tokens_key" + secrets.secret_key_base)
208
+ end
157
209
  end
158
210
 
159
211
  def host
@@ -10,7 +10,7 @@ github_repository = ARGV[1]
10
10
  def get_json(url)
11
11
  uri = URI.parse(url)
12
12
  response = Net::HTTP.get_response(uri)
13
- versions = JSON.load(response.body)
13
+ versions = JSON.parse(response.body)
14
14
  end
15
15
 
16
16
  versions = get_json("https://rubygems.org/api/v1/versions/#{gem_name}.json")
@@ -61,7 +61,7 @@ module Shipit
61
61
  post :create, params: {
62
62
  delivery_url: 'https://example.com/hook',
63
63
  events: %w(deploy rollback),
64
- created_at: 2.months.ago.to_s(:db),
64
+ created_at: 2.months.ago.to_formatted_s(:db),
65
65
  }
66
66
  Hook.last.created_at > 2.seconds.ago
67
67
  end
@@ -107,6 +107,7 @@ module Shipit
107
107
  assert_response :accepted
108
108
  refute_predicate last_deploy, :active?
109
109
  assert_json 'rollback_once_aborted_to.revision.sha', @commit.sha
110
+ assert last_deploy.rollback_once_aborted?
110
111
  end
111
112
  end
112
113
  end
@@ -96,6 +96,24 @@ module Shipit
96
96
  assert_equal 'test', @stack.branch
97
97
  end
98
98
 
99
+ test "#update updates the stack when nil deploy_url" do
100
+ @stack.update(deploy_url: nil)
101
+ @stack.update(continuous_deployment: true)
102
+ assert_nil @stack.deploy_url
103
+ assert @stack.continuous_deployment
104
+
105
+ patch :update, params: {
106
+ id: @stack.to_param,
107
+ continuous_deployment: false,
108
+ }
109
+
110
+ assert_response :ok
111
+ @stack.reload
112
+
113
+ assert_nil @stack.deploy_url
114
+ refute @stack.continuous_deployment
115
+ end
116
+
99
117
  test "#index returns a list of stacks" do
100
118
  stack = Stack.last
101
119
  get :index
@@ -189,6 +207,22 @@ module Shipit
189
207
  assert_response :forbidden
190
208
  assert_json 'message', 'This operation requires the `write:stack` permission'
191
209
  end
210
+
211
+ test "#refresh queues a GithubSyncJob" do
212
+ assert_enqueued_with(job: GithubSyncJob, args: [stack_id: @stack.id]) do
213
+ post :refresh, params: { id: @stack.to_param }
214
+ end
215
+ assert_response :accepted
216
+ end
217
+
218
+ test "#refresh queues a RefreshStatusesJob and RefreshCheckRunsJob" do
219
+ assert_enqueued_with(job: RefreshStatusesJob, args: [stack_id: @stack.id]) do
220
+ assert_enqueued_with(job: RefreshCheckRunsJob, args: [stack_id: @stack.id]) do
221
+ post :refresh, params: { id: @stack.to_param }
222
+ end
223
+ end
224
+ assert_response :accepted
225
+ end
192
226
  end
193
227
  end
194
228
  end
@@ -6,6 +6,7 @@ module Shipit
6
6
  class TasksControllerTest < ActionController::TestCase
7
7
  setup do
8
8
  @stack = shipit_stacks(:shipit)
9
+ @user = shipit_users(:walrus)
9
10
  authenticate!
10
11
  end
11
12
 
@@ -90,6 +91,61 @@ module Shipit
90
91
  assert_response :conflict
91
92
  assert_json 'message', 'A task is already running.'
92
93
  end
94
+
95
+ test "#trigger fails when user does not have deploy permission" do
96
+ @client.permissions.delete('deploy:stack')
97
+ @client.save!
98
+
99
+ assert_no_difference 'Task.count' do
100
+ post :trigger, params: { stack_id: @stack.to_param, task_name: 'restart' }
101
+ end
102
+
103
+ assert_response :forbidden
104
+ assert_json 'message', 'This operation requires the `deploy:stack` permission'
105
+ end
106
+
107
+ test "#abort aborts the task" do
108
+ task = shipit_deploys(:shipit_running)
109
+ task.ping
110
+
111
+ put :abort, params: { stack_id: @stack.to_param, id: task.id }
112
+
113
+ assert_response :accepted
114
+ assert_equal 'aborting', task.reload.status
115
+ end
116
+
117
+ test "#abort sets `aborted_by` to the current user" do
118
+ task = shipit_deploys(:shipit_running)
119
+ task.ping
120
+ request.headers['X-Shipit-User'] = @user.login
121
+
122
+ put :abort, params: { stack_id: @stack.to_param, id: task.id }
123
+
124
+ assert_equal task.reload.aborted_by, @user
125
+ end
126
+
127
+ test "#abort responds with method_not_allowed if the task is not currently running" do
128
+ task = shipit_deploys(:shipit_aborted)
129
+ task.ping
130
+ put :abort, params: { stack_id: @stack.to_param, id: task.id }
131
+
132
+ assert_response :method_not_allowed
133
+ assert_json 'message', 'This task is not currently running.'
134
+ end
135
+
136
+ test "#abort fails when user does not have deploy permission" do
137
+ @client.permissions.delete('deploy:stack')
138
+ @client.save!
139
+ task = shipit_deploys(:shipit_running)
140
+ task.ping
141
+
142
+ assert_no_difference 'Task.count' do
143
+ put :abort, params: { stack_id: @stack.to_param, id: task.id }
144
+ end
145
+
146
+ assert_response :forbidden
147
+ assert_json 'message', 'This operation requires the `deploy:stack` permission'
148
+ end
93
149
  end
94
150
  end
95
151
  end
@@ -36,6 +36,17 @@ module Shipit
36
36
  assert_redirected_to '/github/auth/github?origin=http%3A%2F%2Ftest.host%2F'
37
37
  end
38
38
 
39
+ test "users which require a fresh login are redirected" do
40
+ user = shipit_users(:walrus)
41
+ user.update!(github_access_token: 'some_legacy_value')
42
+ assert_predicate user, :requires_fresh_login?
43
+
44
+ get :index
45
+
46
+ assert_redirected_to '/github/auth/github?origin=http%3A%2F%2Ftest.host%2F'
47
+ assert_nil session[:user_id]
48
+ end
49
+
39
50
  test "current_user must be a member of at least a Shipit.github_teams" do
40
51
  session[:user_id] = shipit_users(:bob).id
41
52
  Shipit.stubs(:github_teams).returns([shipit_teams(:cyclimse_cooks), shipit_teams(:shopify_developers)])
@@ -17,7 +17,6 @@ end
17
17
 
18
18
  module Shipit
19
19
  class Application < Rails::Application
20
- config.load_defaults 6.1
20
+ config.load_defaults 7.0
21
21
  end
22
22
  end
23
-
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2021_08_23_075617) do
13
+ ActiveRecord::Schema.define(version: 2021_11_03_154121) do
14
14
 
15
15
  create_table "api_clients", force: :cascade do |t|
16
16
  t.text "permissions", limit: 65535
@@ -315,7 +315,7 @@ ActiveRecord::Schema.define(version: 2021_08_23_075617) do
315
315
  create_table "teams", force: :cascade do |t|
316
316
  t.integer "github_id", limit: 4
317
317
  t.string "api_url", limit: 255
318
- t.string "slug", limit: 50
318
+ t.string "slug", limit: 255
319
319
  t.string "name", limit: 255
320
320
  t.string "organization", limit: 39
321
321
  t.datetime "created_at", null: false
@@ -7,7 +7,7 @@ second_pending_travis:
7
7
  conclusion: success
8
8
  html_url: "http://www.example.com/run/424242"
9
9
  details_url: "http://www.example.com/build/424242"
10
- created_at: <%= 10.days.ago.to_s(:db) %>
10
+ created_at: <%= 10.days.ago.to_formatted_s(:db) %>
11
11
 
12
12
  check_runs_first_pending_coveralls:
13
13
  stack: check_runs
@@ -15,7 +15,7 @@ check_runs_first_pending_coveralls:
15
15
  github_id: 43
16
16
  title: lets go
17
17
  name: Coverage metrics
18
- created_at: <%= 10.days.ago.to_s(:db) %>
18
+ created_at: <%= 10.days.ago.to_formatted_s(:db) %>
19
19
  conclusion: pending
20
20
  html_url: "http://www.example.com/run/434343"
21
21
  details_url: "http://www.example.com/build/434343"
@@ -26,7 +26,7 @@ check_runs_first_success_coveralls:
26
26
  github_id: 434343
27
27
  title: lets go
28
28
  name: Coverage metrics
29
- created_at: <%= 9.days.ago.to_s(:db) %>
29
+ created_at: <%= 9.days.ago.to_formatted_s(:db) %>
30
30
  conclusion: success
31
31
  html_url: "http://www.example.com/run/434343"
32
32
  details_url: "http://www.example.com/build/434343"