diff --git a/.rvmrc b/.rvmrc index 6ed82ea..0921c1a 100644 --- a/.rvmrc +++ b/.rvmrc @@ -1 +1 @@ -rvm ruby-1.8.7-head@heroku_san --create +rvm gemset use heroku_san --create diff --git a/lib/heroku_san.rb b/lib/heroku_san.rb index 880cb79..48162e2 100644 --- a/lib/heroku_san.rb +++ b/lib/heroku_san.rb @@ -40,6 +40,20 @@ def initialize(config_file) end end + def create_config + template = File.join(File.dirname(__FILE__), 'templates', 'heroku.example.yml') + if File.exists?(@config_file) + false + else + FileUtils.cp(template, @config_file) + true + end + end + + def [](stage) + @app_settings[stage] + end + def all @app_settings.keys end @@ -70,47 +84,68 @@ def apps def each_app raise NoApps if apps.empty? - apps.each do |name| - app = @app_settings[name]['app'] - yield(name, app, "git@heroku.com:#{app}.git", @app_settings[name]['config']) + apps.each do |stage| + yield(stage, "git@heroku.com:#{self[stage]['app']}.git", self[stage]['config']) end end - def migrate(app) - run(app, 'rake', 'db:migrate') - sh "heroku restart --app #{app}" + def stack(stage) + self[stage]['stack'] ||= %x"heroku stack --app #{self[stage]['app']}".split("\n").select { |b| b =~ /^\* / }.first.gsub(/^\* /, '') + end + + def run(stage, command, args = nil) + if stack(stage) =~ /cedar/ + sh_heroku stage, "run #{command} #{args}" + else + sh_heroku stage, "run:#{command} #{args}" + end + end + + def create(stage) + sh "heroku apps:create #{self[stage]['app']}" + end + + def migrate(stage) + run(stage, 'rake', 'db:migrate') + sh_heroku stage, "restart" end - def maintenance(app, action) + def maintenance(stage, action) raise ArgumentError, "Action #{action.inspect} must be one of (:on, :off)", caller if ![:on, :off].include?(action) - sh "heroku maintenance:#{action} --app #{app}" + + sh_heroku stage, "maintenance:#{action}" end - def create_config - template = File.join(File.dirname(__FILE__), 'templates', 'heroku.example.yml') - if File.exists?(@config_file) - false - else - FileUtils.cp(template, @config_file) - true - end + def sharing_add(stage, email) + sh_heroku stage, "sharing:add #{email}" end - def stack(app) - stage, config = @app_settings.find{|stage, settings| settings['app'] == app} - config['stack'] || %x"heroku stack --app #{app}".split("\n").select { |b| b =~ /^\* / }.first.gsub(/^\* /, '') + def sharing_remove(stage, email) + sh_heroku stage, "sharing:remove #{email}" end - def run(app, command, args = nil) - if stack(app) =~ /cedar/ - sh "heroku run #{command} #{args} --app #{app}" - else - sh "heroku run:#{command} #{args} --app #{app}" - end + def long_config(stage) + sh_heroku stage, 'config --long' + end + + def capture(stage) + sh_heroku stage, 'bundles:capture' + end + + def restart(stage) + sh_heroku stage, 'restart' + end + + def logs(stage) + sh_heroku stage, 'logs' end private + def sh_heroku stage, command + sh "heroku #{command} --app #{self[stage]['app']}" + end + def parse_yaml(config_file) if File.exists?(config_file) if defined?(ERB) diff --git a/lib/heroku_san/tasks.rb b/lib/heroku_san/tasks.rb index 769d0f6..d284bc2 100644 --- a/lib/heroku_san/tasks.rb +++ b/lib/heroku_san/tasks.rb @@ -18,32 +18,19 @@ namespace :heroku do desc "Creates the Heroku app" task :create do - each_heroku_app do |stage, app, repo| - sh "heroku create #{app}" + each_heroku_app do |stage, repo, config| + @heroku_san.create(stage) end end desc "Generate the Heroku gems manifest from gem dependencies" task :gems => 'gems:base' do raise HerokuSan::Deprecated - # RAILS_ENV='production' - # Rake::Task[:environment].invoke - # gems = Rails.configuration.gems.reject { |g| g.frozen? && !g.framework_gem? } - # list = gems.collect do |g| - # command, *options = g.send(:install_command) - # options.join(" ") - # end - # - # list.unshift(%Q{rails --version "= #{Rails.version}"}) - # - # File.open(Rails.root.join('.gems'), 'w') do |f| - # f.write(list.join("\n")) - # end end desc 'Add git remotes for all apps in this project' task :remotes do - each_heroku_app do |stage, app, repo| + each_heroku_app do |stage, repo, config| sh "git remote add #{stage} #{repo}" end end @@ -53,8 +40,8 @@ print "Email address of collaborator to add: " $stdout.flush email = $stdin.gets - each_heroku_app do |stage, app, repo| - sh "heroku sharing:add --app #{app} #{email}" + each_heroku_app do |stage, repo, config| + @heroku_san.sharing_add(stage, email) end end @@ -63,15 +50,15 @@ print "Email address of collaborator to remove: " $stdout.flush email = $stdin.gets - each_heroku_app do |stage, app, repo| - sh "heroku sharing:remove --app #{app} #{email}" + each_heroku_app do |stage, repo, config| + @heroku_san.sharing_remove(stage, email) end end desc 'Lists configured apps' task :apps => :all do - each_heroku_app do |stage, app, repo| - puts "#{stage} is shorthand for the Heroku app #{app} located at:" + each_heroku_app do |stage, repo, config| + puts "#{stage} is shorthand for the Heroku app #{@heroku_san[stage]['app']} located at:" puts " #{repo}" print " @ " rev = `git ls-remote -h #{repo}`.split(' ').first @@ -87,10 +74,10 @@ namespace :apps do desc 'Lists configured apps without hitting heroku' task :local => :all do - each_heroku_app do |stage, app, repo| - puts "#{stage} is shorthand for the Heroku app #{app} located at:" + each_heroku_app do |stage, repo, config| + puts "#{stage} is shorthand for the Heroku app #{@heroku_san[stage]['app']} located at:" puts " #{repo}" - tag = tag(stage) + tag = @heroku_san[stage]['tag'] puts " the #{stage} TAG is '#{tag}'" if tag puts end @@ -99,12 +86,12 @@ desc 'Add proper RACK_ENV to each application' task :rack_env => :all do - each_heroku_app do |stage, app, repo| - command = "heroku config --app #{app}" + each_heroku_app do |stage, repo, config| + command = "heroku config --app #{@heroku_san[stage]['app']}" puts command config = Hash[`#{command}`.scan(/^(.+?)\s*=>\s*(.+)$/)] if config['RACK_ENV'] != stage - sh "heroku config:add --app #{app} RACK_ENV=#{stage}" + sh "heroku config:add --app #{@heroku_san[stage]['app']} RACK_ENV=#{stage}" end end end @@ -126,8 +113,8 @@ desc 'Add config:vars to each application.' task :config do - each_heroku_app do |stage, app, repo, config| - command = "heroku config:add --app #{app}" + each_heroku_app do |stage, repo, config| + command = "heroku config:add --app #{@heroku_san[stage]['app']}" config.each do |var, value| command += " #{var}=#{value}" end @@ -138,16 +125,16 @@ namespace :config do desc "Lists config variables as set on Heroku" task :list do - each_heroku_app do |stage, app| + each_heroku_app do |stage, repo, config| puts "#{stage}:" - sh "heroku config --app #{app} --long" + @heroku_san.long_config(stage) end end namespace :list do desc "Lists local config variables without setting them" task :local do - each_heroku_app do |stage, app, repo, config| + each_heroku_app do |stage, repo, config| (config).each do |var, value| puts "#{stage} #{var}: '#{value}'" end @@ -158,54 +145,54 @@ desc 'Runs a rake task remotely' task :rake, [:task] do |t, args| - each_heroku_app do |stage, app, repo| - @heroku_san.run(app, 'rake', args.task) + each_heroku_app do |stage, repo, config| + @heroku_san.run(stage, 'rake', args.task) end end desc "Pushes the given commit (default: HEAD)" task :push, :commit do |t, args| - each_heroku_app do |stage, app, repo| - git_push(args[:commit] || git_parsed_tag(tag(stage)), repo) + each_heroku_app do |stage, repo, config| + git_push(args[:commit] || git_parsed_tag(@heroku_san[stage]['tag']), repo) end end namespace :push do desc "Force-pushes the given commit (default: HEAD)" task :force, :commit do |t, args| - each_heroku_app do |stage, app, repo| - git_push(args[:commit] || git_parsed_tag(tag(stage)), repo, %w[--force]) + each_heroku_app do |stage, repo, config| + git_push(args[:commit] || git_parsed_tag(@heroku_san[stage]['tag']), repo, %w[--force]) end end end desc "Enable maintenance mode" task :maintenance do - each_heroku_app do |stage, app| - @heroku_san.maintenance(app, :on) + each_heroku_app do |stage, repo, config| + @heroku_san.maintenance(stage, :on) end end desc "Enable maintenance mode" task :maintenance_on do - each_heroku_app do |stage, app| - @heroku_san.maintenance(app, :on) + each_heroku_app do |stage, repo, config| + @heroku_san.maintenance(stage, :on) end end desc "Disable maintenance mode" task :maintenance_off do - each_heroku_app do |stage, app| - @heroku_san.maintenance(app, :off) + each_heroku_app do |stage, repo, config| + @heroku_san.maintenance(stage, :off) end end end desc "Pushes the given commit, migrates and restarts (default: HEAD)" task :deploy, [:commit] => [:before_deploy] do |t, args| - each_heroku_app do |stage, app, repo| - git_push(args[:commit] || git_parsed_tag(tag(stage)), repo) - @heroku_san.migrate(app) + each_heroku_app do |stage, repo, config| + git_push(args[:commit] || git_parsed_tag(@heroku_san[stage]['tag']), repo) + @heroku_san.migrate(stage) end Rake::Task[:after_deploy].execute end @@ -213,18 +200,16 @@ namespace :deploy do desc "Force-pushes the given commit, migrates and restarts (default: HEAD)" task :force, [:commit] => [:before_deploy] do |t, args| - each_heroku_app do |stage, app, repo| - git_push(args[:commit] || git_parsed_tag(tag(stage)), repo, %w[--force]) - @heroku_san.migrate(app) + each_heroku_app do |stage, repo, config| + git_push(args[:commit] || git_parsed_tag(@heroku_san[stage]['tag']), repo, %w[--force]) + @heroku_san.migrate(stage) end Rake::Task[:after_deploy].execute end end -# Deprecated. task :force_deploy do raise Deprecated - Rake::Task[:'deploy:force'].invoke end desc "Callback before deploys" @@ -237,47 +222,47 @@ desc "Captures a bundle on Heroku" task :capture do - each_heroku_app do |stage, app, repo| - sh "heroku bundles:capture --app #{app}" + each_heroku_app do |stage, repo, config| + @heroku_san.capture(stage) end end desc "Opens a remote console" task :console do - each_heroku_app do |stage, app, repo| - @heroku_san.run(app, 'console') + each_heroku_app do |stage, repo, config| + @heroku_san.run(stage, 'console') end end desc "Restarts remote servers" task :restart do - each_heroku_app do |stage, app, repo| - sh "heroku restart --app #{app}" + each_heroku_app do |stage, repo, config| + @heroku_san.restart(stage) end end desc "Migrates and restarts remote servers" task :migrate do - each_heroku_app do |stage, app, repo| - @heroku_san.migrate(app) + each_heroku_app do |stage, repo, config| + @heroku_san.migrate(stage) end end desc "Shows the Heroku logs" task :logs do - each_heroku_app do |stage, app, repo| - sh "heroku logs --app #{app}" + each_heroku_app do |stage, repo, config| + @heroku_san.logs(stage) end end namespace :db do task :pull do - each_heroku_app do |stage, app, repo| - sh "heroku pgdumps:capture --app #{app}" - dump = `heroku pgdumps --app #{app}`.split("\n").last.split(" ").first + each_heroku_app do |stage, repo, config| + sh "heroku pgdumps:capture --app #{@heroku_san[stage]['app']}" + dump = `heroku pgdumps --app #{@heroku_san[stage]['app']}`.split("\n").last.split(" ").first sh "mkdir -p #{Rails.root}/db/dumps" file = "#{Rails.root}/db/dumps/#{dump}.sql.gz" - url = `heroku pgdumps:url --app #{app} #{dump}`.chomp + url = `heroku pgdumps:url --app #{@heroku_san[stage]['app']} #{dump}`.chomp sh "wget", url, "-O", file sh "rake db:drop db:create" sh "gunzip -c #{file} | #{Rails.root}/script/dbconsole" @@ -299,8 +284,4 @@ def each_heroku_app(&block) rake all heroku:share" exit(1) -end - -def tag(app) - tag = @heroku_san.app_settings[app]['tag'] -end +end \ No newline at end of file diff --git a/spec/heroku_san_spec.rb b/spec/heroku_san_spec.rb index b28a90e..7b3b99f 100644 --- a/spec/heroku_san_spec.rb +++ b/spec/heroku_san_spec.rb @@ -10,7 +10,10 @@ context "using the example config file" do let(:heroku_config_file) { File.join(SPEC_ROOT, "fixtures", "example.yml") } - let(:template_config_file) { File.join(SPEC_ROOT, "..", "lib/templates", "heroku.example.yml")} + let(:template_config_file) { + path = File.join(SPEC_ROOT, "..", "lib/templates", "heroku.example.yml") + (File.respond_to? :realpath) ? File.realpath(path) : path + } let(:heroku_san) { HerokuSan.new(heroku_config_file) } it "#all" do @@ -74,39 +77,44 @@ expect { heroku_san.each_app do |w,x,y,z| true; end }.to raise_error HerokuSan::NoApps end - it "yields to a block with four args" do + it "yields to a block with args" do heroku_san << 'production' block = double('block') block.should_receive(:action).with('production', - 'awesomeapp', 'git@heroku.com:awesomeapp.git', heroku_san.app_settings['production']['config']) - heroku_san.each_app do |name, app, repos, config| - block.action(name, app, repos, config) + heroku_san.each_app do |stage, repos, config| + block.action(stage, repos, config) end end end + + describe "#[]" do + it "returns a config section" do + heroku_san['production'].should == heroku_san.app_settings['production'] + end + end it "#migrate" do heroku_san.should_receive(:sh).with("heroku run:rake db:migrate --app awesomeapp-staging") heroku_san.should_receive(:sh).with("heroku restart --app awesomeapp-staging") - heroku_san.migrate('awesomeapp-staging') + heroku_san.migrate('staging') end describe "#maintenance" do it ":on" do heroku_san.should_receive(:sh).with("heroku maintenance:on --app awesomeapp") - heroku_san.maintenance('awesomeapp', :on) + heroku_san.maintenance('production', :on) end it ":off" do heroku_san.should_receive(:sh).with("heroku maintenance:off --app awesomeapp") - heroku_san.maintenance('awesomeapp', :off) + heroku_san.maintenance('production', :off) end it ":busy raises an ArgumentError" do expect do - heroku_san.maintenance('awesomeapp', :busy) + heroku_san.maintenance('production', :busy) end.to raise_error ArgumentError, "Action #{:busy.inspect} must be one of (:on, :off)" end end @@ -122,26 +130,33 @@ cedar (beta) EOT } - heroku_san.stack('awesomeapp').should == 'bamboo-mri-1.9.2' + heroku_san.stack('production').should == 'bamboo-mri-1.9.2' end it "returns the stack name from the config if it is set there" do heroku_san.should_not_receive("`") - heroku_san.stack('awesomeapp-staging').should == 'bamboo-ree-1.8.7' + heroku_san.stack('staging').should == 'bamboo-ree-1.8.7' end end describe "#run" do it "runs commands using the pre-cedar format" do heroku_san.should_receive(:sh).with("heroku run:rake foo bar bleh --app awesomeapp-staging") - heroku_san.run('awesomeapp-staging', 'rake', 'foo bar bleh') + heroku_san.run('staging', 'rake', 'foo bar bleh') end it "runs commands using the new cedar format" do heroku_san.should_receive(:sh).with("heroku run worker foo bar bleh --app awesomeapp-demo") - heroku_san.run('awesomeapp-demo', 'worker', 'foo bar bleh') + heroku_san.run('demo', 'worker', 'foo bar bleh') end end end + + describe "#create" do + it "creates an app on heroku" do + heroku_san.should_receive(:sh).with("heroku apps:create awesomeapp") + heroku_san.create('production') + end + end describe "#create_config" do it "creates a new file using the example file" do @@ -158,5 +173,56 @@ heroku_san.create_config.should be_false end end + + describe "#sharing_add" do + it "add collaborators" do + heroku_san.should_receive(:sh).with("heroku sharing:add email@example.com --app awesomeapp") + heroku_san.sharing_add('production', 'email@example.com') + end + end + + describe "#sharing_remove" do + it "removes collaborators" do + heroku_san.should_receive(:sh).with("heroku sharing:remove email@example.com --app awesomeapp") + heroku_san.sharing_remove('production', 'email@example.com') + end + end + + describe "#long_config" do + it "prints out the remote config" do + heroku_san.should_receive(:sh).with("heroku config --long --app awesomeapp") { +< development:test +DATABASE_URL => postgres://thnodhxrzn:T0-UwxLyFgXcnBSHmyhv@ec2-50-19-216-194.compute-1.amazonaws.com/thnodhxrzn +LANG => en_US.UTF-8 +RACK_ENV => production +SHARED_DATABASE_URL => postgres://thnodhxrzn:T0-UwxLyFgXcnBSHmyhv@ec2-50-19-216-194.compute-1.amazonaws.com/thnodhxrzn +EOT + } + heroku_san.long_config('production') + end + end + + describe "#capture" do + it "captures a bundle" do + heroku_san.should_receive(:sh).with("heroku bundles:capture --app awesomeapp") + heroku_san.capture('production') + end + end + + describe "#restart" do + it "restarts an app" do + heroku_san.should_receive(:sh).with("heroku restart --app awesomeapp") + heroku_san.restart('production') + end + end + + describe "#logs" do + it "returns log files" do + heroku_san.should_receive(:sh).with("heroku logs --app awesomeapp") + heroku_san.logs('production') + end + end + end end \ No newline at end of file