travis-cli 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT LICENSE
2
+
3
+ Copyright (c) Sven Fuchs <svenfuchs@artweb-design.de>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # Travis CLI
2
+
3
+ Deployment tool for Travis CI
4
+
5
+ Currently supports the following commands:
6
+
7
+ $ travis deploy [remote] # deploy to remote
8
+ $ travis config [remote] # sync the local config file from travis-keychain and push the config to remote
9
+
10
+ The tool relies on git remotes being defined in `.git/config`. So, in order to deploy to production there needs to be a `production` remote repository defined in the git config.
11
+
12
+ ## Deployment
13
+
14
+ Deploying to `staging` will just push to staging and run the migrations if `-m` was given. One can also pass `-c` in order to configure the app.
15
+
16
+ Deploying to production.
17
+ $ git push staging HEAD:master
18
+ Running migrations.
19
+ $ heroku run rake db:migrate -r staging
20
+
21
+ Deploying to production will also update the production branch and tag the current commit.
22
+
23
+ Updating production branch.
24
+ $ git checkout production
25
+ $ git reset --hard master
26
+ $ git push origin production -f
27
+ Tagging deploy 2011-12-11 16:04.
28
+ $ git tag -a 'deploy.2011-12-11.16-04' -m 'deploy 2011-12-11 16:04'
29
+ $ git push --tags
30
+ $ git checkout master
31
+ Deploying to production.
32
+ $ git push production HEAD:master -f
33
+ Running migrations.
34
+ $ heroku run rake db:migrate -r production
35
+
36
+ WARNING:
37
+
38
+ The production branch is updated using `git reset --hard [branch]` and pushed using `git push origin production -f`.
39
+
40
+ That means that all commits in the `production` branch that are not present in the target branch (e.g. `master`) will be removed from the history of `production`.
41
+
42
+ **So never commit to the production branch directly!**
43
+
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+ require 'tasks/standalone_migrations'
4
+
5
+ desc 'Run specs'
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.pattern = './spec/**/*_spec.rb'
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'rubygems'
6
+ require 'travis/cli'
7
+ require 'thor/runner'
8
+
9
+ klass, task = Thor::Util.find_class_and_task_by_namespace("travis:#{ARGV.shift}")
10
+ ARGV.unshift(task) if task
11
+ klass.start(ARGV, :shell => Thor::Base.shell.new)
@@ -0,0 +1,29 @@
1
+ require 'thor'
2
+
3
+ module Travis
4
+ autoload :Keychain, 'travis/keychain'
5
+
6
+ class Cli < Thor
7
+ autoload :Config, 'travis/cli/config'
8
+ autoload :Deploy, 'travis/cli/deploy'
9
+ autoload :Helper, 'travis/cli/helper'
10
+
11
+ namespace 'travis'
12
+
13
+ desc 'config', 'Sync config between keychain, app and local working directory'
14
+ method_option :restart, :aliases => '-r', :type => :boolean, :default => true
15
+ method_option :backup, :aliases => '-b', :type => :boolean, :default => false
16
+
17
+ def config(remote)
18
+ Config.new(shell, remote, options).invoke
19
+ end
20
+
21
+ desc 'deploy', 'Deploy to the given remote'
22
+ method_option :migrate, :aliases => '-m', :type => :boolean, :default => false
23
+ method_option :configure, :aliases => '-c', :type => :boolean, :default => false
24
+
25
+ def deploy(remote)
26
+ Deploy.new(shell, remote, options).invoke
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,75 @@
1
+ require 'shellwords'
2
+ require 'yaml'
3
+
4
+ module Travis
5
+ class Cli
6
+ class Config
7
+ include Helper
8
+
9
+ attr_reader :shell, :remote, :options
10
+
11
+ def initialize(shell, remote, options)
12
+ @remote = remote
13
+ @options = options
14
+ @shell = shell
15
+ end
16
+
17
+ def invoke
18
+ store
19
+ push
20
+ restart if restart?
21
+ end
22
+
23
+ protected
24
+
25
+ def app
26
+ @app ||= begin
27
+ app = File.basename(Dir.pwd).gsub('travis-', '')
28
+ app = 'web' if app == 'ci'
29
+ app
30
+ end
31
+ end
32
+
33
+ def config
34
+ @config ||= keychain.fetch
35
+ end
36
+
37
+ def keychain
38
+ @keychain ||= Keychain.new(app, shell)
39
+ end
40
+
41
+ def store
42
+ backup if backup?
43
+ File.open(filename, 'w+') { |f| f.write(config) }
44
+ end
45
+
46
+ def push
47
+ say 'Configuring the app ...'
48
+ config = Shellwords.escape(YAML.dump(YAML.load(self.config)[remote]))
49
+ run "heroku config:add travis_config=#{config} -r #{remote}", :echo => "heroku config:add travis_config=... -r #{app}"
50
+ end
51
+
52
+ def restart
53
+ say 'Restarting the app ...'
54
+ run "heroku restart -r #{remote}"
55
+ end
56
+
57
+ def backup
58
+ say 'Backing up the old config file ...'
59
+ run "cp #{filename} #{filename}.backup"
60
+ end
61
+
62
+ def restart?
63
+ !!options['restart']
64
+ end
65
+
66
+ def backup?
67
+ !!options['backup']
68
+ end
69
+
70
+ def filename
71
+ "config/travis.yml"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,83 @@
1
+ module Travis
2
+ class Cli
3
+ class Deploy
4
+ include Helper
5
+
6
+ attr_reader :shell, :remote, :options
7
+
8
+ def initialize(shell, remote, options)
9
+ @remote = remote
10
+ @options = options
11
+ @shell = shell
12
+ end
13
+
14
+ def invoke
15
+ if clean?
16
+ tag if remote == 'production'
17
+ configure if configure?
18
+ push
19
+ migrate if migrate?
20
+ else
21
+ error 'There are unstaged changes.'
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ attr_reader :remote
28
+
29
+ def clean?
30
+ `git status`.include?('working directory clean')
31
+ end
32
+
33
+ def push
34
+ say "Deploying to #{remote}."
35
+ run "git push #{remote} HEAD:master -f".strip
36
+ end
37
+
38
+ def tag
39
+ say "Updating production branch."
40
+ with_branch('production') do |branch|
41
+ run "git reset --hard #{branch}"
42
+ run 'git push origin production -f'
43
+
44
+ say "Tagging #{version}."
45
+ run "git tag -a '#{version.gsub(':', '-').gsub(' ', '.')}' -m '#{version}'"
46
+ run 'git push --tags'
47
+ end
48
+ end
49
+
50
+ def with_branch(target)
51
+ current = branch
52
+ run "git checkout #{target}"
53
+ yield current
54
+ run "git checkout #{current}"
55
+ end
56
+
57
+ def branch
58
+ `git branch --no-color 2> /dev/null` =~ /\* (.*)$/ && $1
59
+ end
60
+
61
+ def version
62
+ @version ||= "deploy #{Time.now.utc.strftime('%Y-%m-%d %H:%M')}"
63
+ end
64
+
65
+ def configure?
66
+ !!options['configure']
67
+ end
68
+
69
+ def configure
70
+ Config.new(shell, remote, :restart => false).invoke
71
+ end
72
+
73
+ def migrate?
74
+ !!options['migrate']
75
+ end
76
+
77
+ def migrate
78
+ say 'Running migrations'
79
+ run "heroku run rake db:migrate -r #{remote}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ module Travis
2
+ class Cli
3
+ module Helper
4
+ protected
5
+
6
+ def run(cmd, options = {})
7
+ cmd = cmd.strip
8
+ puts "$ #{options[:echo] || cmd}" unless options[:echo].is_a?(FalseClass)
9
+ exit unless system(cmd)
10
+ end
11
+
12
+ def say(message)
13
+ shell.say(message, :green)
14
+ end
15
+
16
+ def error(message)
17
+ message = shell.set_color(message, :red)
18
+ shell.error(message)
19
+ exit 1
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ module Travis
2
+ class Keychain
3
+ include Cli::Helper
4
+
5
+ attr_reader :app, :shell, :dir
6
+
7
+ def initialize(app, shell, dir = '../travis-keychain')
8
+ @app = app
9
+ @shell = shell
10
+ @dir = File.expand_path(dir)
11
+ end
12
+
13
+ def fetch
14
+ chdir { pull }
15
+ read
16
+ end
17
+
18
+ protected
19
+
20
+ def pull
21
+ error 'There are unstaged changes in your travis-keychain working directory.' unless clean?
22
+ say 'Fetching the keychain ...'
23
+ run 'git pull'
24
+ end
25
+
26
+ def read
27
+ File.read(File.join(dir, "config/travis.#{app}.yml")) || ''
28
+ end
29
+
30
+ def chdir(&block)
31
+ FileUtils.mkdir_p(dir)
32
+ Dir.chdir(dir, &block)
33
+ end
34
+
35
+ def clean?
36
+ `git status`.include?('working directory clean')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1 @@
1
+ require 'travis/cli'
@@ -0,0 +1,3 @@
1
+ module TravisCli
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,34 @@
1
+ ENV['RAILS_ENV'] = ENV['ENV'] = 'test'
2
+
3
+ RSpec.configure do |c|
4
+ c.mock_with :mocha
5
+ c.before(:each) { Time.now.utc.tap { | now| Time.stubs(:now).returns(now) } }
6
+ end
7
+
8
+ require 'travis/cli'
9
+ require 'mocha'
10
+
11
+ module Mock
12
+ class Shell
13
+ def messages
14
+ @messages ||= []
15
+ end
16
+
17
+ def say(*args)
18
+ messages << args
19
+ end
20
+ alias :error :say
21
+ end
22
+ end
23
+
24
+ module Kernel
25
+ def capture_stdout
26
+ out = StringIO.new
27
+ $stdout = out
28
+ yield
29
+ return out.string
30
+ ensure
31
+ $stdout = STDOUT
32
+ end
33
+ end
34
+
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Travis::Cli::Config do
4
+ let(:shell) { Mock::Shell.new }
5
+ let(:config) { "staging:\n foo: bar" }
6
+
7
+ before :each do
8
+ Travis::Cli::Config.any_instance.stubs(:clean?).returns(true)
9
+ Travis::Cli::Config.any_instance.stubs(:run)
10
+ Travis::Keychain.any_instance.stubs(:fetch).returns(config)
11
+ File.stubs(:open)
12
+ end
13
+
14
+ describe 'sync' do
15
+ it 'fetches the config from the keychain' do
16
+ command = Travis::Cli::Config.new(shell, 'staging', {})
17
+ command.send(:keychain).expects(:fetch).returns(config)
18
+ command.invoke
19
+ end
20
+
21
+ it 'writes the config to the local config file' do
22
+ command = Travis::Cli::Config.new(shell, 'staging', {})
23
+ File.expects(:open).with { |path, mode| path =~ %r(config/travis.yml) }
24
+ command.invoke
25
+ end
26
+
27
+ it 'pushes the config to the given heroku remote' do
28
+ command = Travis::Cli::Config.new(shell, 'staging', {})
29
+ command.expects(:run).with { |cmd, options| cmd =~ /heroku config:add travis_config=.* -r staging/m }
30
+ command.invoke
31
+ end
32
+
33
+ it 'restarts the app when --restart is given' do
34
+ command = Travis::Cli::Config.new(shell, 'staging', 'restart' => true)
35
+ command.expects(:restart)
36
+ command.invoke
37
+ end
38
+
39
+ it 'does not restart the app when --restart is not given' do
40
+ command = Travis::Cli::Config.new(shell, 'staging', {})
41
+ command.expects(:restart).never
42
+ command.invoke
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe Travis::Cli::Deploy do
4
+ let(:shell) { Mock::Shell.new }
5
+
6
+ before :each do
7
+ $stdout = StringIO.new
8
+ Travis::Cli::Deploy.any_instance.stubs(:clean?).returns(true)
9
+ Travis::Cli::Deploy.any_instance.stubs(:branch).returns('master')
10
+ File.stubs(:open)
11
+ end
12
+
13
+ after :each do
14
+ $stdout = STDOUT
15
+ end
16
+
17
+ describe 'with a clean working directory' do
18
+ describe 'given remote "production"' do
19
+ let(:command) { Travis::Cli::Deploy.new(shell, 'production', {}) }
20
+
21
+ before :each do
22
+ command.stubs(:system).returns(true)
23
+ end
24
+
25
+ it 'switches to the production branch' do
26
+ command.expects(:system).with('git checkout production').returns(true)
27
+ command.invoke
28
+ end
29
+
30
+ it 'resets the production branch to the current branch' do
31
+ command.expects(:system).with('git reset --hard master').returns(true)
32
+ command.invoke
33
+ end
34
+
35
+ it 'pushes the production branch to origin' do
36
+ command.expects(:system).with('git push origin production -f').returns(true)
37
+ command.invoke
38
+ end
39
+
40
+ it 'switches back to the previous branch' do
41
+ command.expects(:system).with('git checkout master').returns(true)
42
+ command.invoke
43
+ end
44
+
45
+ it 'tags the current commit ' do
46
+ command.expects(:system).with { |cmd| cmd =~ /git tag -a 'deploy.*' -m 'deploy.*'/ }.returns(true)
47
+ command.invoke
48
+ end
49
+
50
+ it 'pushes the tag to origin' do
51
+ command.expects(:system).with('git push --tags').returns(true)
52
+ command.invoke
53
+ end
54
+
55
+ it 'pushes to the given remote' do
56
+ command.expects(:system).with('git push production HEAD:master -f').returns(true)
57
+ command.invoke
58
+ end
59
+ end
60
+
61
+ describe 'given the remote "staging"' do
62
+ let(:command) { Travis::Cli::Deploy.new(shell, 'staging', {}) }
63
+
64
+ before :each do
65
+ command.stubs(:system).returns(true)
66
+ end
67
+
68
+ it 'does not switch to the production branch' do
69
+ command.expects(:system).with('git checkout production').never
70
+ command.invoke
71
+ end
72
+
73
+ it 'does not tag the current commit if the given remote is "staging"' do
74
+ command.expects(:system).with { |cmd| cmd =~ /git tag -a 'deploy .*' -m 'deploy .*'/ }.never
75
+ command.invoke
76
+ end
77
+
78
+ it 'pushes to the given remote' do
79
+ command.expects(:system).with('git push staging HEAD:master -f').returns(true)
80
+ command.invoke
81
+ end
82
+ end
83
+
84
+ it 'migrates the database if --migrate is given' do
85
+ command = Travis::Cli::Deploy.new(shell, 'production', 'migrate' => true)
86
+ command.stubs(:system).returns(true)
87
+ command.expects(:system).with('heroku run rake db:migrate -r production').returns(true)
88
+ command.invoke
89
+ end
90
+
91
+ it 'configures the application if --configure is given' do
92
+ command = Travis::Cli::Deploy.new(shell, 'production', 'configure' => true)
93
+ command.stubs(:system).returns(true)
94
+ command.expects(:configure)
95
+ command.invoke
96
+ end
97
+ end
98
+
99
+ describe 'with a dirty working directory' do
100
+ before :each do
101
+ end
102
+
103
+ it 'outputs an error message' do
104
+ command = Travis::Cli::Deploy.new(shell, 'production', {})
105
+ command.stubs(:clean?).returns(false)
106
+ command.expects(:error).with('There are unstaged changes.')
107
+ command.invoke
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Travis::Keychain do
4
+ let(:shell) { stub('shell', :say => nil, :error => nil) }
5
+ let(:keychain) { Travis::Keychain.new('hub', shell) }
6
+
7
+ before :each do
8
+ keychain.stubs(:system).returns(true)
9
+ keychain.stubs(:`)
10
+ keychain.stubs(:clean?).returns(true)
11
+ File.stubs(:read)
12
+ end
13
+
14
+ def fetch
15
+ capture_stdout do
16
+ keychain.fetch
17
+ end
18
+ end
19
+
20
+ describe 'fetch' do
21
+ it 'changes to the keychain directory' do
22
+ Dir.expects(:chdir).with { |path| path =~ %r(/travis-keychain$) }
23
+ fetch
24
+ end
25
+
26
+ it 'errors if the working directory is dirty' do
27
+ keychain.stubs(:clean?).returns(false)
28
+ keychain.expects(:error).with('There are unstaged changes in your travis-keychain working directory.')
29
+ fetch
30
+ end
31
+
32
+ it 'pulls changes from origin' do
33
+ keychain.expects(:run).with('git pull')
34
+ fetch
35
+ end
36
+
37
+ it 'reads the configuration' do
38
+ File.expects(:read).with { |path| path =~ %r(config/travis.hub.yml$) }
39
+ fetch
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: travis-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Travis CI
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-11 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: &70356498144940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70356498144940
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70356498144320 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70356498144320
36
+ - !ruby/object:Gem::Dependency
37
+ name: mocha
38
+ requirement: &70356498143500 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70356498143500
47
+ description: ! '[description]'
48
+ email: contact@travis-ci.org
49
+ executables:
50
+ - travis
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/travis/cli/config.rb
55
+ - lib/travis/cli/deploy.rb
56
+ - lib/travis/cli/helper.rb
57
+ - lib/travis/cli.rb
58
+ - lib/travis/keychain.rb
59
+ - lib/travis_cli/version.rb
60
+ - lib/travis_cli.rb
61
+ - spec/spec_helper.rb
62
+ - spec/travis/cli/config_spec.rb
63
+ - spec/travis/cli/deploy_spec.rb
64
+ - spec/travis/keychain_spec.rb
65
+ - Gemfile
66
+ - LICENSE
67
+ - Rakefile
68
+ - README.md
69
+ - bin/travis
70
+ homepage: https://github.com/travis-ci/travis-cli
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project: ! '[none]'
90
+ rubygems_version: 1.8.10
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: ! '[summary]'
94
+ test_files: []
95
+ has_rdoc: