Merge pull request #250 from wvengen/feature-deploy

deployment with Capistrano 3 (in progress)
This commit is contained in:
wvengen 2014-01-27 03:08:24 -08:00
commit 381d5de0a3
8 changed files with 267 additions and 58 deletions

View file

@ -65,9 +65,11 @@ group :development do
gem 'quiet_assets' gem 'quiet_assets'
# Deploy with Capistrano # Deploy with Capistrano
gem 'capistrano', '2.13.5', require: false gem 'capistrano', '~> 3.0', require: false
gem 'capistrano-ext', require: false # https://github.com/capistrano/rails/issues/48#issuecomment-31443739
#gem 'common_deploy', require: false, path: '../../common_deploy' # pending foodcoops/foodsoft#34, git: 'git://github.com/fsmanuel/common_deploy.git' gem 'capistrano-rvm', github: 'capistrano/rvm', require: false
gem 'capistrano-bundler', '>= 1.1.0', require: false
gem 'capistrano-rails', require: false
# Avoid having content-length warnings # Avoid having content-length warnings
gem 'thin' gem 'thin'
end end

View file

@ -4,6 +4,14 @@ GIT
specs: specs:
localize_input (0.1.0) localize_input (0.1.0)
GIT
remote: git://github.com/capistrano/rvm.git
revision: 6aa7cb9d75361c802f466b54d0e345b7237ea3bb
specs:
capistrano-rvm (0.1.1)
capistrano (~> 3.0)
sshkit (~> 1.2)
GIT GIT
remote: git://github.com/fnando/i18n-js.git remote: git://github.com/fnando/i18n-js.git
revision: eab4137f83777963f0ebe6960704a7f64fd8911d revision: eab4137f83777963f0ebe6960704a7f64fd8911d
@ -74,14 +82,16 @@ GEM
bullet (4.7.1) bullet (4.7.1)
activesupport activesupport
uniform_notifier (>= 1.4.0) uniform_notifier (>= 1.4.0)
capistrano (2.13.5) capistrano (3.1.0)
highline i18n
net-scp (>= 1.0.0) rake (>= 10.0.0)
net-sftp (>= 2.0.0) sshkit (~> 1.3)
net-ssh (>= 2.0.14) capistrano-bundler (1.1.1)
net-ssh-gateway (>= 1.1.0) capistrano (~> 3.0)
capistrano-ext (1.2.1) sshkit (>= 1.2.0)
capistrano (>= 1.0.0) capistrano-rails (1.1.0)
capistrano (>= 3.0.0)
capistrano-bundler (>= 1.0.0)
capybara (2.2.1) capybara (2.2.1)
mime-types (>= 1.16) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
@ -143,7 +153,6 @@ GEM
actionpack (>= 3.2, < 5) actionpack (>= 3.2, < 5)
activesupport (>= 3.2, < 5) activesupport (>= 3.2, < 5)
hashery (2.1.1) hashery (2.1.1)
highline (1.6.20)
hike (1.2.3) hike (1.2.3)
i18n (0.6.9) i18n (0.6.9)
i18n-spec (0.4.0) i18n-spec (0.4.0)
@ -191,11 +200,7 @@ GEM
mysql2 (0.3.14) mysql2 (0.3.14)
net-scp (1.1.2) net-scp (1.1.2)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.7.0) net-ssh (2.7.0)
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
nokogiri (1.6.1) nokogiri (1.6.1)
mini_portile (~> 0.5.0) mini_portile (~> 0.5.0)
pdf-reader (1.3.3) pdf-reader (1.3.3)
@ -321,6 +326,10 @@ GEM
rack (~> 1.0) rack (~> 1.0)
tilt (~> 1.1, != 1.3.0) tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.8) sqlite3 (1.3.8)
sshkit (1.3.0)
net-scp (>= 1.1.2)
net-ssh
term-ansicolor
term-ansicolor (1.2.2) term-ansicolor (1.2.2)
tins (~> 0.8) tins (~> 0.8)
therubyracer (0.12.0) therubyracer (0.12.0)
@ -370,8 +379,10 @@ DEPENDENCIES
binding_of_caller binding_of_caller
bootstrap-datepicker-rails bootstrap-datepicker-rails
bullet bullet
capistrano (= 2.13.5) capistrano (~> 3.0)
capistrano-ext capistrano-bundler (>= 1.1.0)
capistrano-rails
capistrano-rvm!
capybara capybara
client_side_validations client_side_validations
client_side_validations-simple_form client_side_validations-simple_form

View file

@ -1,43 +1,48 @@
require 'bundler/setup' #
require 'common_deploy' # Capistrano 3 deployment configuration
#
# http://www.capistranorb.com/
# https://semaphoreapp.com/blog/2013/11/26/capistrano-3-upgrade-guide.html
set :application, 'foodsoft' # defaults that can be updated from the environment
set :domain, 'foodsoft.com' set :branch, ENV["REVISION"] || ENV["BRANCH_NAME"] || "master"
set :user, 'foodsoft'
set :default_stage, 'staging' # staging and production are available via (set :stages, ["staging", "production"])
set :keep_releases, 5
set :repository, 'git://github.com/foodcoops/foodsoft.git'
set(:deploy_to) { "/mnt/apps/#{application}_#{stage}" }
# you probably want to change these
set :application, 'foodsoft' # application name (whatever you like)
set :domain, 'order.foodcoop.test' # host
set :user, 'deploy' # ssh deploy user
set :keep_releases, 10
set :repo_url, 'git://github.com/foodcoops/foodsoft.git'
set :deploy_to, "/www/apps/#{fetch :application}-#{fetch :stage}"
# resque worker # more settings which are probably ok
role :resque_worker, domain set :log_level, :info
role :resque_scheduler, domain set :linked_files, %w{config/database.yml config/app_config.yml config/initializers/secret_token.rb}
set :workers, { "foodsoft_notifier" => 1 } set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}
# assuming one server for everything, with one user for deploy and one for resque
server fetch(:domain), user: fetch(:user), roles: [:web, :app, :resque, :db]
# rvm # if you use RVM, uncomment the line in Capfile, and optionally uncomment rvm settings
# set :rvm_ruby_string, :local # set :rvm_ruby_string, :local
# task hooks
namespace :deploy do
server domain, :web, :app, :db desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
# tell mod_passenger to reload the application
execute :touch, release_path.join('tmp/restart.txt')
end
end
# Loads all needed capistrano extensions after :restart, 'resque:restart'
load_extensions :bundler,
# :rvm, # if you are using rvm on your server uncomment this line
:passenger,
:multistage,
:resque,
:whenever,
:assets
after :finishing, 'deploy:cleanup'
# clean up old releases on each deploy # see lib/capistrano/tasks/plugins.cap
after "deploy:restart", "deploy:cleanup" #before 'bundler:install', 'enable_plugins:auto'
# restart resque end
after "deploy:restart", "resque:restart"
# install rvm and ruby on every deploy
# before 'deploy', 'rvm:install_rvm' # update RVM
# before 'deploy', 'rvm:install_ruby' # install Ruby and create gemset (both if missing)

View file

@ -2,25 +2,40 @@ Deployment
========= =========
Setup Setup
-------- -----
cp config/deploy.rb.SAMPLE config/deploy.rb 1. Initialise your [Capistrano](http://capistranorb.com/) setup
touch config/deploy/staging.rb
touch config/deploy/production.rb ```sh
bundle exec cap install
sed -i 's|^# \(require.*rails.*\)|\1|' Capfile
cp config/deploy.rb.SAMPLE config/deploy.rb
```
When you're using [RVM](http://rvm.io/) on the server you may want to
uncomment the corresponding line in `Capfile`.
2. Adapt your configuration in `config/deploy.rb` and `config/deploy/*.rb`
Deploy Deploy
-------- ------
On your first deploy you should run On your first deploy you should run (choose either staging or production)
bundle exec cap deploy:setup bundle exec cap staging deploy:check
bundle exec cap deploy:check
This will fail, which is ok, because there is no configuration yet. On your
server, there is a directory `shared/config` for each installation, which
contains the configuration. Create `database.yml`, `app_config.yml` and
`initializers/secret_token.rb` and try again.
(See `lib/capistrano/tasks/deploy_initial.cap` for a way to automate this.)
Deploy to staging Deploy to staging
bundle exec cap deploy bundle exec cap staging deploy
Deploy to production Deploy to production
bundle exec cap production deploy bundle exec cap production deploy

View file

@ -0,0 +1,101 @@
# Capistrano tasks for the initial setup
namespace :deploy do
desc 'Creates and initialises a new foodsoft instance'
task :initial do
before 'deploy:check:linked_files', 'deploy:initial:touch_shared'
before 'deploy:updated', 'deploy:initial:secret_token'
before 'deploy:updated', 'deploy:initial:app_config'
before 'deploy:updated', 'deploy:initial:db:config'
before 'deploy:updated', 'deploy:initial:db:create'
before 'deploy:migrate', 'deploy:initial:db:load'
end
after :initial, :deploy
namespace :initial do
namespace :db do
desc 'Generate new database.yml with random password'
task :config => ['deploy:set_rails_env'] do
require 'securerandom'
on roles(:app), in: :groups do
db_name = (fetch(:db_user) or fetch(:application))
db_passwd = SecureRandom.urlsafe_base64(24).to_s
db_yaml = {
fetch(:rails_env).to_s => {
'adapter' => 'mysql2',
'encoding' => 'utf8',
'database' => db_name,
'username' => db_name,
'password' => db_passwd,
}
}
execute :mkdir, '-p', shared_path.join("config")
upload! StringIO.new(db_yaml.to_yaml), shared_path.join("config/database.yml")
end
end
# assumes mysql access setup (~/.my.cnf), with permissions
desc 'Create database new database'
task :create => ['deploy:set_rails_env'] do
on roles(:app), in: :sequence do
config = capture :cat, shared_path.join("config/database.yml")
config = YAML.load(config)[fetch(:rails_env).to_s]
# http://www.grahambrooks.com/blog/create-mysql-database-with-capistrano/
execute :mysql, "--execute='CREATE DATABASE IF NOT EXISTS `#{config['database']}`';"
execute :mysql, "--execute='GRANT ALL ON `#{config['database']}`.* TO \"#{config['username']}\" IDENTIFIED BY \"#{config['password']}\";'"
end
end
desc 'Load database schema'
task :load => ['deploy:set_rails_env'] do
on roles(:app), in: :groups do
# workaround nonexistent release_path on first deploy
path = releases_path.join(capture(:ls, releases_path).split("\n").sort.last)
within path do
with rails_env: fetch(:rails_env) do
execute :rake, 'db:schema:load'
end
end
end
end
end
desc 'Writes a new secret token'
task :secret_token do
require 'securerandom'
on roles(:app), in: :groups do
secret = SecureRandom.hex(64)
text = "Foodsoft::Application.config.secret_token = \"#{secret}\""
execute :mkdir, '-p', shared_path.join("config/initializers")
upload! StringIO.new(text), shared_path.join("config/initializers/secret_token.rb")
end
end
desc 'Creates a default app_config.yml'
task :app_config do
on roles(:app), in: :groups do
execute :mkdir, '-p', shared_path.join("config")
# workaround nonexistent release_path on first deploy
path = releases_path.join(capture(:ls, releases_path).split("\n").sort.last)
execute :cp, path.join("config/app_config.yml.SAMPLE"), shared_path.join("config/app_config.yml")
end
end
desc 'Touches the shared configuration files (for initial deploy)'
task :touch_shared do
on roles(:app), in: :groups do
execute :mkdir, '-p', shared_path.join("config/initializers")
execute :touch, shared_path.join("config/initializers/secret_token.rb")
execute :touch, shared_path.join("config/app_config.yml")
execute :touch, shared_path.join("config/database.yml")
end
end
end
end

View file

@ -0,0 +1,51 @@
# Capistrano tasks for enabling/disabling foodsoft plugins in the Gemfile
#
# Please note that the foodsoft plugins should be present already in the
# Gemfile, either commented out or not.
#
# To automatically enable the desired plugins on deployment, create the
# file `config/plugins.yml` in the shared directory, containing the
# key `enabled` with a list of enabled plugin names (without foodsoft_).
# Then add to your `config/deploy.rb`:
# before 'bundler:install', 'enable_plugins:auto'
desc 'Enable only the foodsoft plugins, cap enable_plugins PLUGINS=wiki,messages'
task :enable_plugins do
on roles(:app), in: :groups do
unless env['PLUGINS'].nil?
enable_foodsoft_plugins(ENV['PLUGINS'].split(/,\s*/))
else
raise 'You need to set the PLUGINS environment variable to enable specific plugins'
end
end
end
namespace :enable_plugins do
desc 'Enable the foodsoft plugins specified in shared/config/plugins.yml, if it exists (key `enabled`).'
task 'auto' do
on roles(:app), in: :groups do
text = capture :cat, shared_path.join('config/plugins.yml'), '||true'
if text
plugins = YAML.load(text)
enable_foodsoft_plugins(plugins['enabled']) if plugins and not plugins['enabled'].nil?
end
end
end
end
# need to run in role
def enable_foodsoft_plugins(plugins)
gemfile = capture :cat, release_path.join('Gemfile')
gemfile.gsub! /^\s*(#)?\s*(gem\s+(['"])foodsoft_(.*?)\3)/ do |c|
(plugins.index($4) ? '' : '#') + $2
end
upload! StringIO.new(gemfile), release_path.join('Gemfile')
# since we updated the Gemfile, we need to run bundler in non-deployment mode
new_bundle_flags = fetch(:bundle_flags).split(/\s+/)
new_bundle_flags.reject! {|o| o=='--deployment'}
new_bundle_flags << '--no-deployment'
set :bundle_flags, new_bundle_flags.join(' ')
end

View file

@ -0,0 +1,24 @@
# capistrano-resque could be used, but it does not support running resque as another user.
# If you want to run resque as another user, setup sudo to allow running commands as that user:
# deploy ALL=(foodsoft_user) NOPASSWD: ALL
# and set `:run_user` to the foodsoft user.
namespace :resque do
%w{start stop restart}.each do |action|
desc "#{action.capitalize} Resque workers"
task action => ['deploy:set_rails_env'] do
on roles(:resque), in: :groups do
within current_path do
cmd = command(:rake, "resque:#{action}_workers", "RAILS_ENV=#{fetch(:rails_env)}")
if fetch(:run_user).nil? or fetch(:run_user) == local_user
execute cmd
else
execute 'sudo', '-u', fetch(:run_user), cmd
end
end
end
end
end
end

View file

@ -4,7 +4,7 @@ def run_worker(queue, count = 1)
puts "Starting #{count} worker(s) with QUEUE: #{queue}" puts "Starting #{count} worker(s) with QUEUE: #{queue}"
ops = {:pgroup => true, :err => ["log/resque_worker_foodsoft_notifier.log", "a"], ops = {:pgroup => true, :err => ["log/resque_worker_foodsoft_notifier.log", "a"],
:out => ["log/resque_worker_foodsoft_notifier.log", "a"]} :out => ["log/resque_worker_foodsoft_notifier.log", "a"]}
env_vars = {"QUEUE" => queue.to_s, "PIDFILE" => "tmp/pids/resque_worker_foodsoft_notifier.pid", "VERBOSE" => "1"} env_vars = {"QUEUE" => queue.to_s, "PIDFILE" => "tmp/pids/resque_worker_foodsoft_notifier.pid"}
count.times { count.times {
## Using Kernel.spawn and Process.detach because regular system() call would ## Using Kernel.spawn and Process.detach because regular system() call would
## cause the processes to quit when capistrano finishes ## cause the processes to quit when capistrano finishes