shipit-engine 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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