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.
- checksums.yaml +4 -4
- data/README.md +5 -106
- data/app/assets/images/timedout.svg +14 -0
- data/app/assets/stylesheets/_pages/_commits.scss +11 -2
- data/app/controllers/shipit/stacks_controller.rb +1 -7
- data/app/controllers/shipit/webhooks_controller.rb +102 -66
- data/app/helpers/shipit/github_url_helper.rb +2 -2
- data/app/helpers/shipit/shipit_helper.rb +3 -31
- data/app/jobs/shipit/destroy_job.rb +9 -0
- data/app/jobs/shipit/github_sync_job.rb +1 -1
- data/app/jobs/shipit/setup_github_hook_job.rb +1 -3
- data/app/models/shipit/anonymous_user.rb +4 -1
- data/app/models/shipit/commit.rb +8 -8
- data/app/models/shipit/commit_deployment.rb +3 -3
- data/app/models/shipit/commit_deployment_status.rb +2 -2
- data/app/models/shipit/deploy.rb +3 -3
- data/app/models/shipit/deploy_spec/file_system.rb +3 -3
- data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +10 -2
- data/app/models/shipit/github_hook.rb +2 -99
- data/app/models/shipit/github_status.rb +1 -1
- data/app/models/shipit/hook.rb +1 -1
- data/app/models/shipit/pull_request.rb +10 -10
- data/app/models/shipit/rollback.rb +1 -1
- data/app/models/shipit/stack.rb +27 -26
- data/app/models/shipit/task.rb +2 -2
- data/app/models/shipit/team.rb +4 -17
- data/app/models/shipit/user.rb +3 -3
- data/app/serializers/shipit/task_serializer.rb +2 -2
- data/app/serializers/shipit/user_serializer.rb +1 -1
- data/app/views/shipit/missing_settings.html.erb +5 -36
- data/app/views/shipit/stacks/new.html.erb +1 -1
- data/app/views/shipit/stacks/settings.html.erb +0 -4
- data/config/routes.rb +3 -13
- data/config/secrets.development.shopify.yml +10 -15
- data/config/secrets.development.yml +1 -1
- data/db/migrate/20180417130436_remove_all_github_hooks.rb +11 -0
- data/lib/shipit.rb +13 -56
- data/lib/shipit/command.rb +1 -1
- data/lib/shipit/engine.rb +2 -8
- data/lib/shipit/github_app.rb +122 -0
- data/lib/shipit/octokit_bot_users_patch.rb +25 -0
- data/lib/shipit/octokit_iterator.rb +2 -2
- data/lib/shipit/version.rb +1 -1
- data/lib/tasks/teams.rake +8 -24
- data/test/controllers/stacks_controller_test.rb +3 -29
- data/test/controllers/webhooks_controller_test.rb +29 -46
- data/test/dummy/config/secrets.yml +40 -10
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/schema.rb +1 -1
- data/test/dummy/db/seeds.rb +0 -1
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/fixtures/payloads/push_master.json +7 -6
- data/test/fixtures/payloads/push_not_master.json +7 -6
- data/test/fixtures/shipit/users.yml +2 -2
- data/test/helpers/hooks_helper.rb +1 -1
- data/test/helpers/payloads_helper.rb +1 -2
- data/test/jobs/destroy_stack_job_test.rb +1 -1
- data/test/models/commits_test.rb +5 -5
- data/test/models/deploy_spec_test.rb +17 -5
- data/test/models/github_hook_test.rb +1 -40
- data/test/models/pull_request_test.rb +11 -11
- data/test/models/stacks_test.rb +4 -10
- data/test/models/team_test.rb +3 -3
- data/test/models/users_test.rb +7 -7
- data/test/test_helper.rb +1 -1
- data/test/unit/github_app_test.rb +44 -0
- data/test/unit/shipit_test.rb +2 -49
- metadata +9 -3
- data/lib/tasks/webhook.rake +0 -6
data/lib/shipit/command.rb
CHANGED
data/lib/shipit/engine.rb
CHANGED
@@ -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.
|
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
|
data/lib/shipit/version.rb
CHANGED
data/lib/tasks/teams.rake
CHANGED
@@ -1,29 +1,13 @@
|
|
1
1
|
namespace :teams do
|
2
|
-
desc "Import the members of each team configured through the
|
2
|
+
desc "Import the members of each team configured through the github.oauth.teams config"
|
3
3
|
task fetch: :environment do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
puts "
|
10
|
-
|
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.
|
12
|
-
|
11
|
+
test "validates that Shipit.github is present" do
|
12
|
+
Rails.application.secrets.stubs(:github).returns(nil)
|
13
13
|
get :index
|
14
|
-
assert_select "#
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 "
|
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 :
|
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 "
|
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
|
-
|
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
|
-
|
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 :
|
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 :
|
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
|
-
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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
|
-
{
|
139
|
+
{action: 'added', team: team_params, organization: {login: 'shopify'}, member: {login: 'walrus'}}
|
157
140
|
end
|
158
141
|
|
159
142
|
def team_params
|