snap_deploy 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rake/file_utils_ext'
3
+
4
+ class SnapDeploy::Provider::Update < Clamp::Command
5
+
6
+ SnapDeploy::CLI.subcommand 'update', 'Update snap deploy', self
7
+
8
+ include SnapDeploy::CLI::DefaultOptions
9
+ include SnapDeploy::Helpers
10
+ include Rake::FileUtilsExt
11
+
12
+ option '--revision',
13
+ 'REVISION',
14
+ "Update to specified revision",
15
+ :default => 'release'
16
+
17
+ def execute
18
+ cd(File.dirname(__FILE__), :verbose => !!verbose?) do
19
+ sh("sudo $(which git) fetch --all", :verbose => !!verbose?)
20
+ sh("sudo $(which git) merge --ff-only origin/#{revision}", :verbose => !!verbose?)
21
+ sh("cd \"$(git rev-parse --show-toplevel)\" && sudo ./install.sh")
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,3 @@
1
+ module SnapDeploy
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'snap_deploy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "snap_deploy"
8
+ spec.version = SnapDeploy::VERSION
9
+ spec.authors = ["Snap CI"]
10
+ spec.email = ["support@snap-ci.com"]
11
+ spec.summary = %q{Deploy your application in a Snap}
12
+ spec.description = %q{A simple rubygem to help continuously deploy your application}
13
+ spec.homepage = "https://snap-ci.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($\) + Dir["bundle/**/*"] - Dir["**/*.gem"] + Dir['bin/*'] - Dir["bundle/ruby/2.0.0/gems/*/{spec,test,specs,tests,examples,doc,doc-api,benchmarks,benchmark,feature,features}/**/*"]
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_runtime_dependency 'ansi'
23
+ spec.add_runtime_dependency "clamp"
24
+ spec.add_runtime_dependency "aws-sdk", '1.53.0'
25
+ spec.add_runtime_dependency "mime-types"
26
+ spec.add_runtime_dependency "heroics"
27
+ spec.add_runtime_dependency "rendezvous"
28
+ end
@@ -0,0 +1,41 @@
1
+ ENV['TEST'] = 'true'
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter '/bundle/ruby/'
5
+ add_filter '/vendor/cache/'
6
+ end
7
+
8
+ require 'snap_deploy'
9
+ require 'webmock/rspec'
10
+
11
+ module SpecHelper
12
+ def revision
13
+ @revision ||= SecureRandom.hex(32)
14
+ end
15
+
16
+ def short_revision
17
+ revision[0..7]
18
+ end
19
+
20
+ def strip_heredoc(str)
21
+ indent = str.scan(/^[ \t]*(?=\S)/).min.size || 0
22
+ str.gsub(/^[ \t]{#{indent}}/, '')
23
+ end
24
+ end
25
+
26
+ RSpec.configure do |config|
27
+ config.include SpecHelper
28
+ config.tty = true
29
+ config.expose_dsl_globally = false
30
+ config.disable_monkey_patching!
31
+
32
+ config.before(:each) do
33
+ @original_env = ENV.to_h.dup
34
+ ENV.clear
35
+ end
36
+
37
+ config.after(:each) do
38
+ ENV.replace(@original_env)
39
+ end
40
+
41
+ end
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+ require 'aws-sdk'
3
+
4
+ RSpec.describe SnapDeploy::Provider::AWS::OpsWorks do
5
+ subject(:cmd) { SnapDeploy::Provider::AWS::OpsWorks.new(nil, {}, {}) }
6
+
7
+ let(:client) { double(:ops_works_client) }
8
+ let(:app_id) { SecureRandom.uuid }
9
+ let(:deployment_id) { SecureRandom.uuid }
10
+ let(:stack_id) { SecureRandom.uuid }
11
+
12
+ let(:ops_works_app) do
13
+ {shortname: 'simplephpapp', stack_id: stack_id}
14
+ end
15
+
16
+ before do
17
+ AWS.stub!
18
+
19
+ allow(cmd).to receive(:client).and_return(client)
20
+
21
+ allow(ENV).to receive(:[]).with('SNAP_PIPELINE_COUNTER').and_return('123')
22
+ allow(ENV).to receive(:[]).with('SNAP_COMMIT_SHORT').and_return(short_revision)
23
+ allow(ENV).to receive(:[]).with('SNAP_COMMIT').and_return(revision)
24
+ allow(ENV).to receive(:[]).with('SNAP_STAGE_TRIGGERED_BY').and_return('john-doe')
25
+ end
26
+
27
+ example 'with migrate option not specified' do
28
+ expect(client).to receive(:describe_apps).with(app_ids: [app_id]).and_return({apps: [ops_works_app]})
29
+ expect(client).to receive(:create_deployment).with(
30
+ stack_id: stack_id,
31
+ app_id: app_id,
32
+ command: {name: 'deploy'},
33
+ comment: "Deploy build 123(rev #{short_revision}) via Snap CI by john-doe",
34
+ custom_json: {"deploy"=>{"simplephpapp"=>{"migrate"=>true, "scm"=>{"revision"=>revision}}}}.to_json
35
+ ).and_return({deployment_id: deployment_id})
36
+
37
+ expect(client).to receive(:describe_deployments).with({deployment_ids: [deployment_id]}).and_return(
38
+ {deployments: [status: 'running']},
39
+ {deployments: [status: 'successful']}
40
+ )
41
+
42
+ expect do
43
+ cmd.run(['--wait', '--app-id', app_id])
44
+ end.to output(strip_heredoc(<<-EOF
45
+ Deployment created: #{deployment_id}
46
+ Deploying .
47
+ Deployment successful.
48
+ EOF
49
+ )).to_stdout
50
+ end
51
+
52
+ example 'with migrate option specified' do
53
+ expect(client).to receive(:describe_apps).with(app_ids: [app_id]).and_return({apps: [ops_works_app]})
54
+ expect(client).to receive(:create_deployment).with(
55
+ stack_id: stack_id,
56
+ app_id: app_id,
57
+ command: {name: 'deploy'},
58
+ comment: "Deploy build 123(rev #{short_revision}) via Snap CI by john-doe",
59
+ custom_json: {"deploy"=>{"simplephpapp"=>{"migrate"=>true, "scm"=>{"revision"=>revision}}}}.to_json
60
+ ).and_return({deployment_id: deployment_id})
61
+
62
+ expect(client).to receive(:describe_deployments).with({deployment_ids: [deployment_id]}).and_return(
63
+ {deployments: [status: 'running']},
64
+ {deployments: [status: 'successful']}
65
+ )
66
+ expect do
67
+ cmd.run(['--wait', '--migrate', '--app-id', app_id])
68
+ end.to output(strip_heredoc(<<-EOF
69
+ Deployment created: #{deployment_id}
70
+ Deploying .
71
+ Deployment successful.
72
+ EOF
73
+ )).to_stdout
74
+
75
+ end
76
+
77
+ example 'with migrate option forced off' do
78
+ expect(client).to receive(:describe_apps).with(app_ids: [app_id]).and_return({apps: [ops_works_app]})
79
+ expect(client).to receive(:create_deployment).with(
80
+ stack_id: stack_id,
81
+ app_id: app_id,
82
+ command: {name: 'deploy'},
83
+ comment: "Deploy build 123(rev #{short_revision}) via Snap CI by john-doe",
84
+ custom_json: {"deploy"=>{"simplephpapp"=>{"migrate"=>false, "scm"=>{"revision"=>revision}}}}.to_json
85
+ ).and_return({deployment_id: deployment_id})
86
+
87
+ expect(client).to receive(:describe_deployments).with({deployment_ids: [deployment_id]}).and_return(
88
+ {deployments: [status: 'running']},
89
+ {deployments: [status: 'successful']}
90
+ )
91
+
92
+ expect do
93
+ cmd.run(['--wait', '--no-migrate', '--app-id', app_id])
94
+ end.to output(strip_heredoc(<<-EOF
95
+ Deployment created: #{deployment_id}
96
+ Deploying .
97
+ Deployment successful.
98
+ EOF
99
+ )).to_stdout
100
+ end
101
+
102
+ example 'when deployment fails' do
103
+ expect(client).to receive(:describe_apps).with(app_ids: [app_id]).and_return({apps: [ops_works_app]})
104
+ expect(client).to receive(:create_deployment).with(
105
+ stack_id: stack_id,
106
+ app_id: app_id,
107
+ command: {name: 'deploy'},
108
+ comment: "Deploy build 123(rev #{short_revision}) via Snap CI by john-doe",
109
+ custom_json: {"deploy"=>{"simplephpapp"=>{"migrate"=>false, "scm"=>{"revision"=>revision}}}}.to_json
110
+ ).and_return({deployment_id: deployment_id})
111
+
112
+ expect(client).to receive(:describe_deployments).with({deployment_ids: [deployment_id]}).and_return(
113
+ {deployments: [status: 'running']},
114
+ {deployments: [status: 'failed']}
115
+ )
116
+
117
+ expect do
118
+ expect do
119
+ cmd.run(['--wait', '--no-migrate', '--app-id', app_id])
120
+ end.to raise_error('Deployment failed.')
121
+ end.to output(strip_heredoc(<<-EOF
122
+ Deployment created: #{deployment_id}
123
+ Deploying .
124
+ Deployment failed.
125
+ EOF
126
+ )).to_stdout
127
+ end
128
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'aws-sdk'
3
+
4
+ RSpec.describe SnapDeploy::Provider::AWS::S3 do
5
+ subject(:cmd) { SnapDeploy::Provider::AWS::S3.new(nil, {}, {}) }
6
+
7
+ before do
8
+ AWS.stub!
9
+
10
+ allow(ENV).to receive(:[]).with(anything).and_call_original
11
+ allow(ENV).to receive(:[]).with('AWS_ACCESS_KEY_ID').and_return(SecureRandom.hex)
12
+ allow(ENV).to receive(:[]).with('AWS_SECRET_ACCESS_KEY').and_return(SecureRandom.hex)
13
+ allow(ENV).to receive(:[]).with('SNAP_PIPELINE_COUNTER').and_return('123')
14
+ allow(ENV).to receive(:[]).with('SNAP_COMMIT_SHORT').and_return(short_revision)
15
+ allow(ENV).to receive(:[]).with('SNAP_COMMIT').and_return(revision)
16
+ allow(ENV).to receive(:[]).with('SNAP_STAGE_TRIGGERED_BY').and_return('john-doe')
17
+ end
18
+
19
+ example 'with local dir not specified' do
20
+ expect(Dir).to receive(:chdir).with(Dir.pwd)
21
+ cmd.run(['--bucket', 'example.com'])
22
+ end
23
+
24
+ example 'with local dir specified' do
25
+ expect(Dir).to receive(:chdir).with('_build')
26
+ cmd.run(['--bucket', 'example.com', '--local-dir', '_build'])
27
+ end
28
+
29
+ example "Sends MIME type" do
30
+ expect(Dir).to receive(:glob).and_yield(__FILE__)
31
+ expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:content_type => 'application/x-ruby'))
32
+ cmd.run(['--bucket', 'example.com'])
33
+ end
34
+
35
+ example "Sets Cache and Expiration" do
36
+ expect(Dir).to receive(:glob).and_yield(__FILE__)
37
+ expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:cache_control => 'max-age=99999999', :expires => '2012-12-21 00:00:00 -0000'))
38
+ cmd.run(['--bucket', 'example.com', '--cache-control', 'max-age=99999999', '--expires', '2012-12-21 00:00:00 -0000'])
39
+ end
40
+
41
+ example "Sets ACL" do
42
+ expect(Dir).to receive(:glob).and_yield(__FILE__)
43
+ expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:acl => "public_read"))
44
+ cmd.run(['--bucket', 'example.com', '--acl', 'public_read'])
45
+ end
46
+
47
+ example "when detect_encoding is set" do
48
+ path = 'foo.js'
49
+ expect(Dir).to receive(:glob).and_yield(path)
50
+ expect(cmd).to receive(:'`').at_least(1).times.with("file #{path}").and_return('gzip compressed')
51
+ allow(File).to receive(:read).with(path).and_return("")
52
+ expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:create).with(anything(), anything(), hash_including(:content_encoding => 'gzip'))
53
+ cmd.run(['--bucket', 'example.com', '--detect-encoding'])
54
+ end
55
+
56
+ example "when dot_match is set" do
57
+ expect(Dir).to receive(:glob).with("**/*", File::FNM_DOTMATCH)
58
+ cmd.run(['--bucket', 'example.com', '--detect-encoding', '--include-dot-files'])
59
+ end
60
+ end
@@ -0,0 +1,189 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SnapDeploy::Provider::Heroku do
4
+ subject(:cmd) { SnapDeploy::Provider::Heroku.new(nil, {}, {}) }
5
+
6
+ let(:token) { SecureRandom.hex }
7
+ let(:branch) { SecureRandom.hex }
8
+
9
+ before do
10
+ allow(cmd).to receive(:token).and_return(token)
11
+
12
+ ENV['SNAP_BRANCH'] = branch
13
+ end
14
+
15
+ describe 'execution behavior' do
16
+ it 'should invoke methods in order' do
17
+ expect(cmd).to receive(:check_auth).ordered
18
+ expect(cmd).to receive(:maybe_create_app).ordered
19
+ expect(cmd).to receive(:setup_configuration).ordered
20
+ expect(cmd).to receive(:git_push).ordered
21
+ expect(cmd).to receive(:maybe_db_migrate).ordered
22
+
23
+ cmd.run(['--app-name', 'foo'])
24
+ end
25
+ end
26
+
27
+ describe 'check authentication' do
28
+ it 'should raise error on auth failure' do
29
+ stub_request(:get, 'https://api.heroku.com/account').
30
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
31
+ to_return(:status => 401, :body => {}.to_json, :headers => {})
32
+
33
+ expect do
34
+ cmd.parse(['--app-name', 'foo'])
35
+ cmd.send :check_auth
36
+ end.to raise_error(RuntimeError, 'Could not connect to heroku to check your credentials. The server returned status code 401.')
37
+ end
38
+
39
+ it 'should raise error when could not connect' do
40
+ stub_request(:get, 'https://api.heroku.com/account').
41
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
42
+ to_raise(EOFError)
43
+
44
+ expect do
45
+ cmd.parse(['--app-name', 'foo'])
46
+ cmd.send :check_auth
47
+ end.to raise_error(EOFError)
48
+ end
49
+ end
50
+
51
+ describe 'create app' do
52
+ it 'should create app if one does not exist' do
53
+ stub_request(:get, 'https://api.heroku.com/apps/foo').
54
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
55
+ to_return(:status => 404)
56
+
57
+ stub_request(:post, 'https://api.heroku.com/apps').
58
+ with(:headers => { 'Authorization' => "Bearer #{token}" })
59
+
60
+ cmd.parse(['--app-name', 'foo'])
61
+ cmd.send(:maybe_create_app)
62
+ end
63
+
64
+ [401, 403].each do |code|
65
+ it "should raise error if we cannot verify if app exists (#{code})" do
66
+ stub_request(:get, 'https://api.heroku.com/apps/foo').
67
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
68
+ to_return(:status => code)
69
+
70
+ allow(cmd).to receive(:setup_configuration)
71
+ allow(cmd).to receive(:git_push)
72
+
73
+ expect do
74
+ cmd.parse(['--app-name', 'foo'])
75
+ cmd.send(:maybe_create_app)
76
+ end.to raise_error(RuntimeError, "You are not authorized to check if the app exists, perhaps you don't own that app?. The server returned status code #{code}.")
77
+ end
78
+ end
79
+
80
+ it 'should create app if one does not exist' do
81
+ stub_request(:get, 'https://api.heroku.com/apps/foo').
82
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
83
+ to_return(:status => 404)
84
+
85
+ stub_request(:post, 'https://api.heroku.com/apps').
86
+ with(:headers => { 'Authorization' => "Bearer #{token}" })
87
+
88
+ allow(cmd).to receive(:setup_configuration)
89
+ allow(cmd).to receive(:git_push)
90
+
91
+ cmd.parse(['--app-name', 'foo', '--region', 'jhumri-taliya', '--stack-name', 'some-stack'])
92
+ cmd.send(:maybe_create_app)
93
+ expect(a_request(:post, 'https://api.heroku.com/apps').
94
+ with(:body => { name: 'foo', region: 'jhumri-taliya', stack: 'some-stack' }, :headers => { 'Authorization' => "Bearer #{token}" })).to have_been_made
95
+ end
96
+ end
97
+
98
+ describe 'setup_configuration' do
99
+ it 'should not send config vars if one is not specified' do
100
+ cmd.parse(['--app-name', 'foo'])
101
+ cmd.send(:setup_configuration)
102
+ end
103
+
104
+ it 'should not set any config vars if there is no delta' do
105
+ stub_request(:get, 'https://api.heroku.com/apps/foo/config-vars').
106
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
107
+ to_return(:body => { 'FOO' => 'bar', 'BOO' => 'baz' }.to_json, :headers => { 'Content-Type' => 'application/json' })
108
+
109
+ cmd.parse(['--app-name', 'foo', '--config-var', 'FOO=bar', '--config-var', 'BOO=baz'])
110
+ cmd.send(:setup_configuration)
111
+
112
+ expect(a_request(:any, "api.heroku.com")).not_to have_been_made
113
+ end
114
+
115
+ it 'should set any config vars if there is a delta' do
116
+ stub_request(:get, 'https://api.heroku.com/apps/foo/config-vars').
117
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
118
+ to_return(:body => { 'FOO' => 'oldfoo', 'BOO' => 'oldboo' }.to_json, :headers => { 'Content-Type' => 'application/json' })
119
+
120
+ stub_request(:patch, 'https://api.heroku.com/apps/foo/config-vars')
121
+
122
+ cmd.parse(['--app-name', 'foo', '--config-var', 'FOO=newfoo', '--config-var', 'BOO=oldboo', '--config-var', 'NEW_VAR=new_value'])
123
+ cmd.send(:setup_configuration)
124
+
125
+ expect(a_request(:patch, "https://api.heroku.com/apps/foo/config-vars").
126
+ with(:body => {'FOO' => 'newfoo', 'NEW_VAR' => 'new_value'}, :headers => { 'Authorization' => "Bearer #{token}"})).to have_been_made
127
+ end
128
+
129
+ it 'should set buildpack url if one is specified' do
130
+ stub_request(:get, 'https://api.heroku.com/apps/foo/config-vars').
131
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
132
+ to_return(:body => { 'FOO' => 'oldfoo', 'BOO' => 'oldboo' }.to_json, :headers => { 'Content-Type' => 'application/json' })
133
+
134
+ stub_request(:patch, 'https://api.heroku.com/apps/foo/config-vars')
135
+
136
+ cmd.parse(['--app-name', 'foo', '--buildpack-url', 'https://github.com/heroku/heroku-buildpack-ruby'])
137
+ cmd.send(:setup_configuration)
138
+
139
+ expect(a_request(:patch, "https://api.heroku.com/apps/foo/config-vars").
140
+ with(:body => {'BUILDPACK_URL' => 'https://github.com/heroku/heroku-buildpack-ruby'}, :headers => { 'Authorization' => "Bearer #{token}"})).to have_been_made
141
+ end
142
+
143
+ it 'should set buildpack url if one is specified along with any configs' do
144
+ stub_request(:get, 'https://api.heroku.com/apps/foo/config-vars').
145
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
146
+ to_return(:body => { 'FOO' => 'oldfoo', 'BOO' => 'oldboo' }.to_json, :headers => { 'Content-Type' => 'application/json' })
147
+
148
+ stub_request(:patch, 'https://api.heroku.com/apps/foo/config-vars')
149
+
150
+ cmd.parse(['--app-name', 'foo', '--config-var', 'FOO=newfoo', '--config-var', 'BOO=oldboo', '--config-var', 'NEW_VAR=new_value', '--buildpack-url', 'https://github.com/heroku/heroku-buildpack-ruby'])
151
+ cmd.send(:setup_configuration)
152
+
153
+ expect(a_request(:patch, "https://api.heroku.com/apps/foo/config-vars").
154
+ with(:body => {'FOO' => 'newfoo', 'NEW_VAR' => 'new_value', 'BUILDPACK_URL' => 'https://github.com/heroku/heroku-buildpack-ruby'}, :headers => { 'Authorization' => "Bearer #{token}"})).to have_been_made
155
+ end
156
+
157
+ it 'should work with config vars which have a "=" in their values' do
158
+ stub_request(:get, 'https://api.heroku.com/apps/foo/config-vars').
159
+ with(:headers => { 'Authorization' => "Bearer #{token}" }).
160
+ to_return(:body => { 'FOO' => 'oldfoo' }.to_json, :headers => { 'Content-Type' => 'application/json' })
161
+
162
+ stub_request(:patch, 'https://api.heroku.com/apps/foo/config-vars')
163
+
164
+ cmd.parse(['--app-name', 'foo', '--config-var', 'FOO=new=foo==bar'])
165
+ cmd.send(:setup_configuration)
166
+
167
+ expect(a_request(:patch, "https://api.heroku.com/apps/foo/config-vars").
168
+ with(:body => {'FOO' => 'new=foo==bar'}, :headers => { 'Authorization' => "Bearer #{token}"})).to have_been_made
169
+ end
170
+ end
171
+
172
+ describe 'git push' do
173
+ it 'should push to heroku via https' do
174
+ cmd.parse(['--app-name', 'foo'])
175
+ system('true')
176
+ expect(cmd).to receive(:sh).with('git push https://git.heroku.com/foo.git HEAD:refs/heads/master -f').and_yield(true, $?)
177
+ cmd.send(:git_push)
178
+ end
179
+
180
+ it 'should raise error when push fails' do
181
+ cmd.parse(['--app-name', 'foo'])
182
+ system('exit -1')
183
+ expect(cmd).to receive(:sh).with('git push https://git.heroku.com/foo.git HEAD:refs/heads/master -f').and_yield(false, $?)
184
+ expect do
185
+ cmd.send(:git_push)
186
+ end.to raise_error(RuntimeError, 'Could not push to heroku remote. The exit code was 255.')
187
+ end
188
+ end
189
+ end