diff --git a/Gemfile b/Gemfile index 71ba7eeb..c49bfd14 100644 --- a/Gemfile +++ b/Gemfile @@ -65,9 +65,11 @@ group :development do gem 'quiet_assets' # Deploy with Capistrano - gem 'capistrano', '2.13.5', require: false - gem 'capistrano-ext', require: false - #gem 'common_deploy', require: false, path: '../../common_deploy' # pending foodcoops/foodsoft#34, git: 'git://github.com/fsmanuel/common_deploy.git' + gem 'capistrano', '~> 3.0', require: false + # https://github.com/capistrano/rails/issues/48#issuecomment-31443739 + 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 gem 'thin' end diff --git a/Gemfile.lock b/Gemfile.lock index ee236034..f4dadb98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,14 @@ GIT specs: 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 remote: git://github.com/fnando/i18n-js.git revision: eab4137f83777963f0ebe6960704a7f64fd8911d @@ -74,14 +82,16 @@ GEM bullet (4.7.1) activesupport uniform_notifier (>= 1.4.0) - capistrano (2.13.5) - highline - net-scp (>= 1.0.0) - net-sftp (>= 2.0.0) - net-ssh (>= 2.0.14) - net-ssh-gateway (>= 1.1.0) - capistrano-ext (1.2.1) - capistrano (>= 1.0.0) + capistrano (3.1.0) + i18n + rake (>= 10.0.0) + sshkit (~> 1.3) + capistrano-bundler (1.1.1) + capistrano (~> 3.0) + sshkit (>= 1.2.0) + capistrano-rails (1.1.0) + capistrano (>= 3.0.0) + capistrano-bundler (>= 1.0.0) capybara (2.2.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -143,7 +153,6 @@ GEM actionpack (>= 3.2, < 5) activesupport (>= 3.2, < 5) hashery (2.1.1) - highline (1.6.20) hike (1.2.3) i18n (0.6.9) i18n-spec (0.4.0) @@ -191,11 +200,7 @@ GEM mysql2 (0.3.14) net-scp (1.1.2) net-ssh (>= 2.6.5) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) net-ssh (2.7.0) - net-ssh-gateway (1.2.0) - net-ssh (>= 2.6.5) nokogiri (1.6.1) mini_portile (~> 0.5.0) pdf-reader (1.3.3) @@ -321,6 +326,10 @@ GEM rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sqlite3 (1.3.8) + sshkit (1.3.0) + net-scp (>= 1.1.2) + net-ssh + term-ansicolor term-ansicolor (1.2.2) tins (~> 0.8) therubyracer (0.12.0) @@ -370,8 +379,10 @@ DEPENDENCIES binding_of_caller bootstrap-datepicker-rails bullet - capistrano (= 2.13.5) - capistrano-ext + capistrano (~> 3.0) + capistrano-bundler (>= 1.1.0) + capistrano-rails + capistrano-rvm! capybara client_side_validations client_side_validations-simple_form diff --git a/config/deploy.rb.SAMPLE b/config/deploy.rb.SAMPLE index b28611fe..9d946b74 100644 --- a/config/deploy.rb.SAMPLE +++ b/config/deploy.rb.SAMPLE @@ -1,43 +1,56 @@ -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' -set :domain, 'foodsoft.com' -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}" } +# defaults that can be updated from the environment +set :branch, ENV["REVISION"] || ENV["BRANCH_NAME"] || "master" +# 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 :default_stage, 'staging' # default environment, see config/deploy/ +set :keep_releases, 10 +set :repo_url, 'git://github.com/foodcoops/foodsoft.git' +set :unique_app_name, fetch(:application) # for more complex setups, this can be customised + # XXX how to get rails environment in here? +set :deploy_to, "/www/apps/#{fetch :unique_app_name}" -# resque worker -role :resque_worker, domain -role :resque_scheduler, domain -set :workers, { "foodsoft_notifier" => 1 } +# more settings which are probably ok +set :log_level, :info +set :linked_files, %w{config/database.yml config/app_config.yml config/initializers/secret_token.rb} +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 +# 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 -load_extensions :bundler, - # :rvm, # if you are using rvm on your server uncomment this line - :passenger, - :multistage, - :resque, - :whenever, - :assets + after :restart, 'resque:restart' + after :restart, :clear_cache do + on roles(:web), in: :groups, limit: 3, wait: 10 do + within release_path do + execute :rake, 'cache:clear' + end + end + end -# clean up old releases on each deploy -after "deploy:restart", "deploy:cleanup" + after :finishing, 'deploy:cleanup' -# restart resque -after "deploy:restart", "resque:restart" +end -# 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) \ No newline at end of file diff --git a/lib/capistrano/tasks/deploy_initial.cap b/lib/capistrano/tasks/deploy_initial.cap new file mode 100644 index 00000000..ac481777 --- /dev/null +++ b/lib/capistrano/tasks/deploy_initial.cap @@ -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(:unique_app_name).gsub(/[^-_a-z0-9]/i, '') + 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 + diff --git a/lib/capistrano/tasks/resque.cap b/lib/capistrano/tasks/resque.cap new file mode 100644 index 00000000..5d06b5b3 --- /dev/null +++ b/lib/capistrano/tasks/resque.cap @@ -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