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