snap_deploy 0.1.3

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