shipit-engine 0.21.0 → 0.22.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -106
  3. data/app/assets/images/timedout.svg +14 -0
  4. data/app/assets/stylesheets/_pages/_commits.scss +11 -2
  5. data/app/controllers/shipit/stacks_controller.rb +1 -7
  6. data/app/controllers/shipit/webhooks_controller.rb +102 -66
  7. data/app/helpers/shipit/github_url_helper.rb +2 -2
  8. data/app/helpers/shipit/shipit_helper.rb +3 -31
  9. data/app/jobs/shipit/destroy_job.rb +9 -0
  10. data/app/jobs/shipit/github_sync_job.rb +1 -1
  11. data/app/jobs/shipit/setup_github_hook_job.rb +1 -3
  12. data/app/models/shipit/anonymous_user.rb +4 -1
  13. data/app/models/shipit/commit.rb +8 -8
  14. data/app/models/shipit/commit_deployment.rb +3 -3
  15. data/app/models/shipit/commit_deployment_status.rb +2 -2
  16. data/app/models/shipit/deploy.rb +3 -3
  17. data/app/models/shipit/deploy_spec/file_system.rb +3 -3
  18. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +10 -2
  19. data/app/models/shipit/github_hook.rb +2 -99
  20. data/app/models/shipit/github_status.rb +1 -1
  21. data/app/models/shipit/hook.rb +1 -1
  22. data/app/models/shipit/pull_request.rb +10 -10
  23. data/app/models/shipit/rollback.rb +1 -1
  24. data/app/models/shipit/stack.rb +27 -26
  25. data/app/models/shipit/task.rb +2 -2
  26. data/app/models/shipit/team.rb +4 -17
  27. data/app/models/shipit/user.rb +3 -3
  28. data/app/serializers/shipit/task_serializer.rb +2 -2
  29. data/app/serializers/shipit/user_serializer.rb +1 -1
  30. data/app/views/shipit/missing_settings.html.erb +5 -36
  31. data/app/views/shipit/stacks/new.html.erb +1 -1
  32. data/app/views/shipit/stacks/settings.html.erb +0 -4
  33. data/config/routes.rb +3 -13
  34. data/config/secrets.development.shopify.yml +10 -15
  35. data/config/secrets.development.yml +1 -1
  36. data/db/migrate/20180417130436_remove_all_github_hooks.rb +11 -0
  37. data/lib/shipit.rb +13 -56
  38. data/lib/shipit/command.rb +1 -1
  39. data/lib/shipit/engine.rb +2 -8
  40. data/lib/shipit/github_app.rb +122 -0
  41. data/lib/shipit/octokit_bot_users_patch.rb +25 -0
  42. data/lib/shipit/octokit_iterator.rb +2 -2
  43. data/lib/shipit/version.rb +1 -1
  44. data/lib/tasks/teams.rake +8 -24
  45. data/test/controllers/stacks_controller_test.rb +3 -29
  46. data/test/controllers/webhooks_controller_test.rb +29 -46
  47. data/test/dummy/config/secrets.yml +40 -10
  48. data/test/dummy/db/development.sqlite3 +0 -0
  49. data/test/dummy/db/schema.rb +1 -1
  50. data/test/dummy/db/seeds.rb +0 -1
  51. data/test/dummy/db/test.sqlite3 +0 -0
  52. data/test/fixtures/payloads/push_master.json +7 -6
  53. data/test/fixtures/payloads/push_not_master.json +7 -6
  54. data/test/fixtures/shipit/users.yml +2 -2
  55. data/test/helpers/hooks_helper.rb +1 -1
  56. data/test/helpers/payloads_helper.rb +1 -2
  57. data/test/jobs/destroy_stack_job_test.rb +1 -1
  58. data/test/models/commits_test.rb +5 -5
  59. data/test/models/deploy_spec_test.rb +17 -5
  60. data/test/models/github_hook_test.rb +1 -40
  61. data/test/models/pull_request_test.rb +11 -11
  62. data/test/models/stacks_test.rb +4 -10
  63. data/test/models/team_test.rb +3 -3
  64. data/test/models/users_test.rb +7 -7
  65. data/test/test_helper.rb +1 -1
  66. data/test/unit/github_app_test.rb +44 -0
  67. data/test/unit/shipit_test.rb +2 -49
  68. metadata +9 -3
  69. data/lib/tasks/webhook.rake +0 -6
@@ -143,7 +143,7 @@ module Shipit
143
143
  end
144
144
 
145
145
  def yield_control
146
- @control_block.call if @control_block
146
+ @control_block&.call
147
147
  end
148
148
 
149
149
  def read_stream(io)
@@ -29,16 +29,10 @@ module Shipit
29
29
  ActiveModel::ArraySerializer._root = false
30
30
  ActiveModel::Serializer.include(Engine.routes.url_helpers)
31
31
 
32
- if Shipit.github_oauth_credentials
32
+ if Shipit.github.oauth?
33
33
  OmniAuth::Strategies::GitHub.configure path_prefix: '/github/auth'
34
34
  app.middleware.use OmniAuth::Builder do
35
- provider(
36
- :github,
37
- Shipit.github_oauth_id,
38
- Shipit.github_oauth_secret,
39
- scope: 'email,repo_deployment',
40
- client_options: Shipit.github_oauth_options,
41
- )
35
+ provider(:github, *Shipit.github.oauth_config)
42
36
  end
43
37
  end
44
38
  end
@@ -0,0 +1,122 @@
1
+ module Shipit
2
+ class GitHubApp
3
+ DOMAIN = 'github.com'.freeze
4
+ AuthenticationFailed = Class.new(StandardError)
5
+
6
+ attr_reader :oauth_teams, :domain, :bot_login
7
+
8
+ def initialize(config)
9
+ @config = (config || {}).with_indifferent_access
10
+ @domain = @config[:domain] || DOMAIN
11
+ @webhook_secret = @config[:webhook_secret].presence
12
+ @bot_login = @config[:bot_login]
13
+
14
+ oauth = (@config[:oauth] || {}).with_indifferent_access
15
+ @oauth_id = oauth[:id]
16
+ @oauth_secret = oauth[:secret]
17
+ @oauth_teams = Array.wrap(oauth[:teams] || oauth[:teams])
18
+ end
19
+
20
+ def login
21
+ raise NotImplementedError, 'Handle App login / user'
22
+ end
23
+
24
+ def api
25
+ @client = new_client if !defined?(@client) || @client.access_token != token
26
+ @client
27
+ end
28
+
29
+ def verify_webhook_signature(signature, message)
30
+ return true unless webhook_secret
31
+
32
+ algorithm, signature = signature.split("=", 2)
33
+ return false unless algorithm == 'sha1'
34
+
35
+ SecureCompare.secure_compare(signature, OpenSSL::HMAC.hexdigest(algorithm, webhook_secret, message))
36
+ end
37
+
38
+ def token
39
+ return 't0kEn' if Rails.env.test? # TODO: figure out something cleaner
40
+ return unless private_key && app_id && installation_id
41
+
42
+ Rails.cache.fetch('github:integration:token', expires_in: 50.minutes, race_condition_ttl: 10.minutes) do
43
+ token = Octokit::Client.new(bearer_token: authentication_payload).create_app_installation_access_token(
44
+ installation_id,
45
+ accept: 'application/vnd.github.machine-man-preview+json',
46
+ ).token
47
+ raise AuthenticationFailed if token.blank?
48
+ token
49
+ end
50
+ end
51
+
52
+ def oauth?
53
+ oauth_id.present? && oauth_secret.present?
54
+ end
55
+
56
+ def oauth_config
57
+ options = {}
58
+ if enterprise?
59
+ options = {
60
+ site: api_endpoint,
61
+ authorize_url: url('/login/oauth/authorize'),
62
+ token_url: url('/login/oauth/access_token'),
63
+ }
64
+ end
65
+
66
+ [
67
+ oauth_id,
68
+ oauth_secret,
69
+ scope: 'email,repo_deployment',
70
+ client_options: options,
71
+ ]
72
+ end
73
+
74
+ def url(*path)
75
+ @url ||= "https://#{domain}".freeze
76
+ path.empty? ? @url : File.join(@url, *path.map(&:to_s))
77
+ end
78
+
79
+ def api_endpoint
80
+ url('/api/v3/') if enterprise?
81
+ end
82
+
83
+ def enterprise?
84
+ domain != DOMAIN
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :webhook_secret, :oauth_id, :oauth_secret
90
+
91
+ def app_id
92
+ @app_id ||= @config.fetch(:app_id)
93
+ end
94
+
95
+ def installation_id
96
+ @installation_id ||= @config.fetch(:installation_id)
97
+ end
98
+
99
+ def private_key
100
+ @private_key ||= @config.fetch(:private_key)
101
+ end
102
+
103
+ def new_client
104
+ client = Octokit::Client.new(
105
+ access_token: token,
106
+ api_endpoint: api_endpoint,
107
+ )
108
+ client.middleware = Shipit.new_faraday_stack
109
+ client
110
+ end
111
+
112
+ def authentication_payload
113
+ payload = {
114
+ iat: Time.now.to_i,
115
+ exp: 10.minutes.from_now.to_i,
116
+ iss: app_id,
117
+ }
118
+ key = OpenSSL::PKey::RSA.new(private_key)
119
+ JWT.encode(payload, key, 'RS256')
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,25 @@
1
+ require 'octokit'
2
+
3
+ # https://github.com/octokit/octokit.rb/pull/1006
4
+ if Octokit::VERSION >= '5'
5
+ raise 'This patch should be removed'
6
+ else
7
+ module Octokit
8
+ module Connection
9
+ protected
10
+
11
+ def request(method, path, data, options = {})
12
+ if data.is_a?(Hash)
13
+ options[:query] = data.delete(:query) || {}
14
+ options[:headers] = data.delete(:headers) || {}
15
+ if accept = data.delete(:accept)
16
+ options[:headers][:accept] = accept
17
+ end
18
+ end
19
+
20
+ @last_response = response = agent.call(method, Addressable::URI.parse(path.to_s).normalize.to_s, data, options)
21
+ response.data
22
+ end
23
+ end
24
+ end
25
+ end
@@ -6,8 +6,8 @@ module Shipit
6
6
  if relation
7
7
  @response = relation.get(per_page: 100)
8
8
  else
9
- yield Shipit.github_api
10
- @response = Shipit.github_api.last_response
9
+ yield Shipit.github.api
10
+ @response = Shipit.github.api.last_response
11
11
  end
12
12
  end
13
13
 
@@ -1,3 +1,3 @@
1
1
  module Shipit
2
- VERSION = '0.21.0'.freeze
2
+ VERSION = '0.22.0'.freeze
3
3
  end
@@ -1,29 +1,13 @@
1
1
  namespace :teams do
2
- desc "Import the members of each team configured through the github_oauth.teams config and attempt to set a webhook to keep the list updated"
2
+ desc "Import the members of each team configured through the github.oauth.teams config"
3
3
  task fetch: :environment do
4
- handles = Shipit.github_teams_handles
5
- if handles.empty?
6
- puts "github_oauth.teams is empty"
7
- else
8
- handles.each do |handle|
9
- puts "Fetching @#{handle} members"
10
- begin
11
- team = Shipit::Team.find_or_create_by_handle(handle)
12
- team.refresh_members!
13
- rescue Octokit::Unauthorized, Octokit::NotFound => error
14
- puts "Failed to fetch @#{handle} members. Do you have enough permissions?"
15
- puts "#{error.class}: #{error.message}"
16
- end
17
-
18
- if team
19
- puts "Ensuring webhook presence for #{team.organization}"
20
- begin
21
- team.setup_hooks(async: false)
22
- rescue Octokit::Unauthorized, Octokit::NotFound => error
23
- puts "Failed to install webhook on #{team.organization}. Do you have enough permissions?"
24
- puts "#{error.class}: #{error.message}"
25
- end
26
- end
4
+ Shipit.github_teams.each do |team|
5
+ puts "Fetching @#{team.handle} members"
6
+ begin
7
+ team.refresh_members!
8
+ rescue Octokit::Unauthorized, Octokit::NotFound => error
9
+ puts "Failed to fetch @#{team.handle} members. Do you have enough permissions?"
10
+ puts "#{error.class}: #{error.message}"
27
11
  end
28
12
  end
29
13
  end
@@ -8,24 +8,10 @@ module Shipit
8
8
  session[:user_id] = shipit_users(:walrus).id
9
9
  end
10
10
 
11
- test "validates that Shipit.github_oauth_id is present" do
12
- Shipit.stubs(github_oauth_credentials: {'secret' => 'abc'})
11
+ test "validates that Shipit.github is present" do
12
+ Rails.application.secrets.stubs(:github).returns(nil)
13
13
  get :index
14
- assert_select "#github_oauth_id .missing"
15
- assert_select ".missing", count: 1
16
- end
17
-
18
- test "validates that Shipit.github_oauth_secret is present" do
19
- Shipit.stubs(github_oauth_credentials: {'id' => 'abc'})
20
- get :index
21
- assert_select "#github_oauth_secret .missing"
22
- assert_select ".missing", count: 1
23
- end
24
-
25
- test "validates that Shipit.github_api_credentials is present" do
26
- Shipit.stubs(github_api_credentials: {})
27
- get :index
28
- assert_select "#github_api .missing"
14
+ assert_select "#github_app .missing"
29
15
  assert_select ".missing", count: 1
30
16
  end
31
17
 
@@ -142,18 +128,6 @@ module Shipit
142
128
  assert_redirected_to stack_settings_path(@stack)
143
129
  end
144
130
 
145
- test "#sync_webhooks queues #{Stack::REQUIRED_HOOKS.count} SetupGithubHookJob" do
146
- assert_enqueued_jobs(Stack::REQUIRED_HOOKS.count) do
147
- post :sync_webhooks, params: {id: @stack.to_param}
148
- end
149
- assert_redirected_to stack_settings_path(@stack)
150
- end
151
-
152
- test "#sync_webhooks displays a flash message" do
153
- post :sync_webhooks, params: {id: @stack.to_param}
154
- assert_equal 'Webhooks syncing scheduled', flash[:success]
155
- end
156
-
157
131
  test "#clear_git_cache queues a ClearGitCacheJob" do
158
132
  assert_enqueued_with(job: ClearGitCacheJob, args: [@stack]) do
159
133
  post :clear_git_cache, params: {id: @stack.to_param}
@@ -9,10 +9,9 @@ module Shipit
9
9
 
10
10
  test ":push with the target branch queues a GithubSyncJob" do
11
11
  request.headers['X-Github-Event'] = 'push'
12
- params = payload(:push_master)
13
12
 
14
13
  assert_enqueued_with(job: GithubSyncJob, args: [stack_id: @stack.id]) do
15
- post :push, params: {stack_id: @stack.id}.merge(params)
14
+ post :create, body: payload(:push_master), as: :json
16
15
  end
17
16
  end
18
17
 
@@ -20,21 +19,21 @@ module Shipit
20
19
  request.headers['X-Github-Event'] = 'push'
21
20
  params = payload(:push_not_master)
22
21
  assert_no_enqueued_jobs do
23
- post :push, params: {stack_id: @stack.id}.merge(params)
22
+ post :create, body: params, as: :json
24
23
  end
25
24
  end
26
25
 
27
26
  test ":state create a Status for the specific commit" do
28
27
  request.headers['X-Github-Event'] = 'status'
29
28
 
30
- status_payload = payload(:status_master)
31
29
  commit = shipit_commits(:first)
32
30
 
33
31
  assert_difference 'commit.statuses.count', 1 do
34
- post :state, params: {stack_id: @stack.id}.merge(status_payload)
32
+ post :create, body: payload(:status_master), as: :json
35
33
  end
36
34
 
37
35
  status = commit.statuses.last
36
+ status_payload = JSON.parse(payload(:status_master))
38
37
  assert_equal status_payload['target_url'], status.target_url
39
38
  assert_equal status_payload['state'], status.state
40
39
  assert_equal status_payload['description'], status.description
@@ -44,108 +43,92 @@ module Shipit
44
43
 
45
44
  test ":state with a unexisting commit respond with 200 OK" do
46
45
  request.headers['X-Github-Event'] = 'status'
47
- params = {'sha' => 'notarealcommit', 'state' => 'pending', 'branches' => [{'name' => 'master'}]}
48
- post :state, params: {stack_id: @stack.id}.merge(params)
46
+ params = {'sha' => 'notarealcommit', 'state' => 'pending', 'branches' => [{'name' => 'master'}]}.to_json
47
+ post :create, body: params, as: :json
49
48
  assert_response :ok
50
49
  end
51
50
 
52
51
  test ":state in an untracked branche bails out" do
53
52
  request.headers['X-Github-Event'] = 'status'
54
- params = {'sha' => 'notarealcommit', 'state' => 'pending', 'branches' => []}
55
- post :state, params: {stack_id: @stack.id}.merge(params)
53
+ params = {'sha' => 'notarealcommit', 'state' => 'pending', 'branches' => []}.to_json
54
+ post :create, body: params, as: :json
56
55
  assert_response :ok
57
56
  end
58
57
 
59
- test ":push returns head :ok if request is ping" do
58
+ test "returns head :ok if request is ping" do
60
59
  @request.headers['X-Github-Event'] = 'ping'
61
60
 
62
61
  assert_no_enqueued_jobs do
63
- post :state, params: {stack_id: @stack.id, zen: 'Git is beautiful'}
62
+ post :create, body: {zen: 'Git is beautiful'}.to_json, as: :json
64
63
  assert_response :ok
65
64
  end
66
65
  end
67
66
 
68
- test ":state returns head :ok if request is ping" do
69
- @request.headers['X-Github-Event'] = 'ping'
70
-
71
- assert_no_enqueued_jobs do
72
- post :state, params: {stack_id: @stack.id}
73
- assert_response :ok
74
- end
75
- end
76
-
77
- test ":state verifies webhook signature" do
67
+ test "verifies webhook signature" do
78
68
  commit = shipit_commits(:first)
79
69
 
80
- params = {"sha" => commit.sha, "state" => "pending", "target_url" => "https://ci.example.com/1000/output"}
70
+ payload = {"sha" => commit.sha, "state" => "pending", "target_url" => "https://ci.example.com/1000/output"}.to_json
81
71
  signature = 'sha1=4848deb1c9642cd938e8caa578d201ca359a8249'
82
72
 
83
73
  @request.headers['X-Github-Event'] = 'push'
84
74
  @request.headers['X-Hub-Signature'] = signature
85
75
 
86
- GithubHook.any_instance.expects(:verify_signature).with(signature, URI.encode_www_form(params)).returns(false)
87
-
88
- post :push, params: {stack_id: @stack.id}.merge(params)
89
- assert_response :unprocessable_entity
90
- end
91
-
92
- test ":push verifies webhook signature" do
93
- params = {"ref" => "refs/heads/master"}
94
- signature = 'sha1=ad1d939e9acd6bdc2415a2dd5951be0f2a796ce0'
95
-
96
- @request.headers['X-Github-Event'] = 'push'
97
- @request.headers['X-Hub-Signature'] = signature
98
-
99
- GithubHook.any_instance.expects(:verify_signature).with(signature, URI.encode_www_form(params)).returns(false)
76
+ Shipit.github.expects(:verify_webhook_signature).with(signature, payload).returns(false)
100
77
 
101
- post :push, params: {stack_id: @stack.id}.merge(params)
78
+ post :create, body: payload, as: :json
102
79
  assert_response :unprocessable_entity
103
80
  end
104
81
 
105
82
  test ":membership creates the mentioned team on the fly" do
83
+ @request.headers['X-Github-Event'] = 'membership'
106
84
  assert_difference -> { Team.count }, 1 do
107
- post :membership, params: membership_params.merge(team: {
85
+ post :create, as: :json, body: membership_params.merge(team: {
108
86
  id: 48,
109
87
  name: 'Ouiche Cooks',
110
88
  slug: 'ouiche-cooks',
111
89
  url: 'https://example.com',
112
- })
90
+ }).to_json
113
91
  assert_response :ok
114
92
  end
115
93
  end
116
94
 
117
95
  test ":membership creates the mentioned user on the fly" do
118
- Shipit.github_api.expects(:user).with('george').returns(george)
96
+ @request.headers['X-Github-Event'] = 'membership'
97
+ Shipit.github.api.expects(:user).with('george').returns(george)
119
98
  assert_difference -> { User.count }, 1 do
120
- post :membership, params: membership_params.merge(member: {login: 'george'})
99
+ post :create, body: membership_params.merge(member: {login: 'george'}).to_json, as: :json
121
100
  assert_response :ok
122
101
  end
123
102
  end
124
103
 
125
104
  test ":membership can delete an user membership" do
105
+ @request.headers['X-Github-Event'] = 'membership'
126
106
  assert_difference -> { Membership.count }, -1 do
127
- post :membership, params: membership_params.merge(_action: 'removed')
107
+ post :create, body: membership_params.merge(action: 'removed').to_json, as: :json
128
108
  assert_response :ok
129
109
  end
130
110
  end
131
111
 
132
112
  test ":membership can append an user membership" do
113
+ @request.headers['X-Github-Event'] = 'membership'
133
114
  assert_difference -> { Membership.count }, 1 do
134
- post :membership, params: membership_params.merge(member: {login: 'bob'})
115
+ post :create, body: membership_params.merge(member: {login: 'bob'}).to_json, as: :json
135
116
  assert_response :ok
136
117
  end
137
118
  end
138
119
 
139
120
  test ":membership can append an user twice" do
121
+ @request.headers['X-Github-Event'] = 'membership'
140
122
  assert_no_difference -> { Membership.count } do
141
- post :membership, params: membership_params
123
+ post :create, body: membership_params.to_json, as: :json
142
124
  assert_response :ok
143
125
  end
144
126
  end
145
127
 
146
128
  test ":membership can delete an user twice" do
129
+ @request.headers['X-Github-Event'] = 'membership'
147
130
  assert_no_difference -> { Membership.count } do
148
- post :membership, params: membership_params.merge(_action: 'removed', member: {login: 'bob'})
131
+ post :create, body: membership_params.merge(action: 'removed', member: {login: 'bob'}).to_json, as: :json
149
132
  assert_response :ok
150
133
  end
151
134
  end
@@ -153,7 +136,7 @@ module Shipit
153
136
  private
154
137
 
155
138
  def membership_params
156
- {_action: 'added', team: team_params, organization: {login: 'shopify'}, member: {login: 'walrus'}}
139
+ {action: 'added', team: team_params, organization: {login: 'shopify'}, member: {login: 'walrus'}}
157
140
  end
158
141
 
159
142
  def team_params