├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── capistrano-bundle_rsync.gemspec ├── example ├── Capfile ├── Gemfile ├── README.md ├── Vagrantfile └── config │ ├── deploy.rb │ └── deploy │ ├── git.rb │ ├── local_git.rb │ └── skip_bundle.rb ├── lib ├── capistrano-bundle_rsync.rb └── capistrano │ ├── bundle_rsync.rb │ ├── bundle_rsync │ ├── base.rb │ ├── bundler.rb │ ├── config.rb │ ├── defaults.rb │ ├── git.rb │ ├── git_turbo.rb │ ├── local_git.rb │ ├── plugin.rb │ └── scm.rb │ └── tasks │ └── bundle_rsync.rake └── spec ├── capistrano └── bundle_rsync │ └── config_spec.rb └── spec_helper.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '2.7' 18 | - '3.0' 19 | - '3.1' 20 | - '3.2' 21 | - '3.3' 22 | - '3.4' 23 | - 'head' 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | - name: Run Test 33 | run: bundle exec rspec 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | example/.local_repo 19 | example/.capistrano 20 | example/.vagrant 21 | example/log 22 | example/vendor 23 | example/try_rails4 24 | .ruby-version 25 | vendor/ 26 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.6.0 (2025/03/14) 2 | 3 | New Features: 4 | 5 | * Introduce `set :bundle_rsync_scm, :git_turbo` strategy for super-fast deployment (@aeroastro) 6 | * Support `set :bundle_rsync_app_path, ` option to specify where to run `bundle` (@aeroastro) 7 | * Support `set :bundle_rsync_bundle_install_jobs, ` option to control `bundle install --jobs ` (thanks to @grezar) 8 | 9 | Enhancements: 10 | 11 | * Improve plugin support (`set_defaults`) for Capistrano 3.7+ (@aeroastro) 12 | * Accelerate deployment by syncing multiple configs with one `rsync` call (@aeroastro) 13 | 14 | Fixes: 15 | 16 | * Allow `capistrano-bundle_rsync` to deploy apps with different Ruby version from that of deploying environment (@aeroastro) 17 | * Fix bundler deprecation of `Bundler.with_clean_env` (@aeroastro) 18 | 19 | # 0.5.2 (2019/02/14) 20 | 21 | Fixes: 22 | 23 | * Fix num_components by tree_repo (thanks to Hiroaki Kubota) 24 | 25 | # 0.5.1 (2017/05/29) 26 | 27 | Changes: 28 | 29 | * Use git rev-list instead of git rev-parse to get current_version as https://github.com/capistrano/capistrano/pull/1339 (thanks to aeroastro) 30 | 31 | # 0.5.0 (2017/04/21) 32 | 33 | Enhancements: 34 | 35 | * Support capistrano-3.7 (thanks to troter) 36 | 37 | # 0.4.9 (2017/02/26) 38 | 39 | Fixes: 40 | 41 | * Fix `local_git` was not working because of a change in 0.4.7 (thanks to hkobayash) 42 | 43 | # 0.4.8 (2016/11/18) 44 | 45 | Enhancements: 46 | 47 | * Suppress ls-remote log by filtering remote refs with HEAD (thanks to aeroastro) 48 | 49 | # 0.4.7 (2016/07/22) 50 | 51 | Fixes: 52 | 53 | * Fix cleaning .local_repo/releases was not working if skip_bundle is specified (thanks to @kozyty) 54 | 55 | # 0.4.6 (2016/02/03) 56 | 57 | Enhancements: 58 | 59 | * Support repo_tree with local_git (thanks to @mizzy) 60 | 61 | # 0.4.5 (2015/08/06) 62 | 63 | Enhancements: 64 | 65 | * Support bundle install with --standalone option (thanks to @niku4i) 66 | 67 | # 0.4.4 (2015/07/09) 68 | 69 | Enhancements: 70 | 71 | * Avoid to remove existing .bundle/config using BUNDLE_APP_CONFIG (thanks to @hkobayash) 72 | 73 | # 0.4.3 (2015/03/16) 74 | 75 | Fixes: 76 | 77 | * Avoid possible name conflictions 78 | 79 | # 0.4.2 (2015/03/16) 80 | 81 | Fixes7 82 | 83 | * Fix bugs incorporated with v0.4.1 84 | 85 | # 0.4.1 (2015/03/16) 86 | 87 | Fixes: 88 | 89 | * Fix --hosts and --roles options to work (thanks to @niku4i) 90 | * Fix to config.bundle_rsync_max_parallels to be integer always (thanks to @niku4i) 91 | * Fix for the case `bundle_rsync_ssh_options` and `ssh_options` are not configured (thanks to @niku4i) 92 | 93 | # 0.4.0 (2015/03/04) 94 | 95 | Enhancements: 96 | 97 | * Support `repo_tree` to deploy subtree of a repository (thanks @lecoueyl) 98 | 99 | # 0.3.3 (2015/01/30) 100 | 101 | Fixes: 102 | 103 | * Fix to cleanup .local_repo/releases with keep_releases 104 | 105 | # 0.3.2 (2014/12/11) 106 | 107 | Changes: 108 | 109 | * Remove `.bundle/config` to make it possible to install development gems before rake assets:precompile 110 | 111 | # 0.3.1 (2014/08/05) 112 | 113 | Changes: 114 | 115 | * Remove `bundle_rsync_shared_rsync_options` to reduce complexity 116 | 117 | # 0.3.0 (2014/08/05) 118 | 119 | Enhancements: 120 | 121 | * Add `bundle_rsync_shared_dirs` and `bundle_rsync_shared_rsync_options` options 122 | 123 | Fixes: 124 | 125 | * Fix `bundle_rsync_config_files` 126 | 127 | # 0.2.4 (2014/07/28) 128 | 129 | Changes: 130 | 131 | * Change process parallel to thread parallel to save memory usage as capistrano/sshkit does 132 | 133 | # 0.2.3 (2014/07/18) 134 | 135 | Enhancements: 136 | 137 | * Add `bundle_rsync_skip_bundle` option 138 | 139 | # 0.2.2 (2014/07/17) 140 | 141 | Changes: 142 | 143 | * Split rsync tasks and reorder tasks to be better 144 | 145 | # 0.2.1 (2014/07/11) 146 | 147 | Fixes: 148 | 149 | * Stop to rsync binstubs not to override bin of rails 4 150 | 151 | # 0.2.0 (2014/07/11) 152 | 153 | Enhancements: 154 | 155 | * Support `bundle_rsync_keep_releases` configuration 156 | * Newly add `set :bundle_rsync_scm 'local_git'` configuration 157 | 158 | Changes: 159 | 160 | * Rename `bundle_rsync_local_repo_path` to `bundle_rsync_local_mirror_path` 161 | 162 | # 0.1.1 (2014/07/09) 163 | 164 | Enhancements: 165 | 166 | * Support `bundle_rsync_rsync_options` configuration 167 | * Support `bundle_rsync_rsync_bwlimit` configuration 168 | 169 | # 0.1.0 (2014/07/08) 170 | 171 | First version 172 | 173 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in capistrano-bundle_rsync.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Naotoshi Seo 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capistrano::BundleRsync for Capistrano v3 2 | 3 | Deploy an application and bundled gems via rsync 4 | 5 | ## What is this for? 6 | 7 | Capistrano::BundleRsync builds (bundle) gems only once on a deploy machine and transfers gems to your production machines via rsync, which saves you from building gems on each your production machine. 8 | 9 | Also saves you from having to install Git and Build Tools on your production machine. 10 | 11 | ## How it works 12 | 13 | Capistrano::BundleRsync works as followings: 14 | 15 | 1. Do `git clone --mirror URL .local_repo/mirror` on a deploy (local) machine. 16 | 2. Extract a branch by `git archive {branch}` to `.local_repo/releases/{datetime}` 17 | 3. Do `bundle --without development test --path .local_repo/bundle` on a deploy (local) machine. 18 | 4. Deploy the `release` directory to remote machines by `rsync`. 19 | 5. Deploy the `bundle` directory to remote machines by `rsync`. 20 | 21 | ## Prerequisites 22 | 23 | The deploy machine and remote machines must be on same architectures (i386, x86\_64) because 24 | C exntension gems are built on the deploy machine and transfered by rsync. 25 | 26 | Requiremens on remote machines: 27 | 28 | 1. rbenv (rvm should work, but not verified) 29 | 2. ruby 30 | 31 | Requirements on a deploy machine: 32 | 33 | 1. rbenv (rvm should work, but not verified) 34 | 2. git 35 | 3. Build Tools required to build C exntension gems (like gcc). 36 | 4. A ruby to execute capistrano (you may use the ruby of 5.) 37 | 5. The same ruby used at your remote machines 38 | 6. A ssh key to login to your remote machines via ssh (Passowrd authentication is not supported) 39 | 40 | Notice that it is not required to have Git and Build Tools on each remote machine. 41 | 42 | ## Configuration 43 | 44 | Set Capistrano variables with `set name, value`. 45 | 46 | Name | Default | Description 47 | --------------|---------|------------ 48 | repo_url | `.` | The path or URL to a Git repository to clone from. 49 | repo_tree | nil | Specify the subtree path of the repository to deploy. 50 | branch | `master` | The Git branch to checkout. 51 | ssh_options | `{}` | Configuration of ssh :user and :keys. 52 | keep\_releases | 5 | The number of releases to keep. 53 | scm | nil | Must be `bundle_rsync` to use capistrano-bundle_rsync. 54 | bundle_rsync_scm | `git` | SCM Strategy inside `bundle_rsync`. `git`, `local_git`, or `git_turbo` can be specified. 55 | bundle_rsync_local_base_path | `$(pwd)/.local_repo` | The base directory to clone repository 56 | bundle_rsync_local_mirror_path | `#{base_path}/mirror"` | Path where to mirror your repository 57 | bundle_rsync_local_releases_path | `"#{base_path}/releases"` | The releases base directory to checkout your repository 58 | bundle_rsync_local_release_path | `"#{releases_path}/#{datetime}"` | The full path directory to checkout your repository. If you specify this, `keep_releases` for local releases path is disabled because `datetime` directories are no longer created. This parameter is set as `repo_url` in the case of `local_git` as default. 59 | bundle_rsync_local_bundle_path | `"#{base_path}/bundle"` | Path where to bundle install gems. 60 | bundle_rsync_ssh_options | `ssh_options` | Configuration of ssh for rsync. Default uses the value of `ssh_options` 61 | bundle_rsync_keep_releases | `keep_releases` | The number of releases to keep on .local_repo 62 | bundle_rsync_max_parallels | number of hosts | Number of concurrency. The default is the number of hosts to deploy. 63 | bundle_rsync_rsync_bwlimit | nil | Configuration of rsync --bwlimit (KBPS) option. Not Avabile if `bundle_rsync_rsync_options` is specified. 64 | bundle_rsync_rsync_options | `-az --delete` | Configuration of rsync options. 65 | bundle_rsync_config_files | `nil` | Additional files to rsync. Specified files are copied into `config` directory. 66 | bundle_rsync_shared_dirs | `nil` | Additional directories to rsync. Specified directories are copied into `shared` directory. 67 | bundle_rsync_app_path | `.` | A relative app path from local `bundle_rsync_local_release_path` and from remote `release_path`. `bundle` commands are executed here, and directories like `.bundle/config` and `config` will be located in this directory. 68 | bundle_rsync_skip_bundle | false | (Secret option) Do not `bundle` and rsync bundle. 69 | bundle_rsync_bundle_install_jobs | `nil` | Configuration of bundle install with --jobs option. 70 | bundle_rsync_bundle_install_standalone | `nil` | bundle install with --standalone option. Set one of `true`, `false`, an `Array` of groups, or a white space separated `String`. 71 | bundle_rsync_bundle_without | `[:development, :test]` | Configuration of bundle install with --without option. 72 | 73 | ### BundleRsync SCM Strategies 74 | 75 | * git 76 | * Original Strategy Using Git 77 | * local_git 78 | * Directly Rsync Local (Non-Bare) Git Directory at `repo_url` Path to Remote Release Path 79 | * git_turbo 80 | * Super-Fast Deployment Utilizing Git Worktree, Hardlink, and Rsync 81 | 82 | ## Task Orders 83 | 84 | ``` 85 | $ cap stage deploy --trace | grep Execute 86 | ** Execute bundle_rsync:check 87 | ** Execute bundle_rsync:clone 88 | ** Execute bundle_rsync:update 89 | ** Execute bundle_rsync:create_release 90 | ** Execute bundle_rsync:bundler:install 91 | ** Execute bundle_rsync:rsync_release 92 | ** Execute bundle_rsync:rsync_shared 93 | ** Execute bundle_rsync:bundler:rsync 94 | ** Execute bundle_rsync:clean_release 95 | ** Execute bundle_rsync:set_current_revision 96 | ``` 97 | 98 | ## Installation 99 | 100 | Add this line to your application's Gemfile: 101 | 102 | gem 'capistrano-bundle_rsync' 103 | 104 | And then execute: 105 | 106 | $ bundle 107 | 108 | Or install it yourself as: 109 | 110 | $ gem install capistrano-bundle_rsync 111 | 112 | ## Usage 113 | 114 | Add followings to your Gemfile (capistrano-rvm should work, but not verified): 115 | 116 | ```ruby 117 | gem 'capistrano' 118 | gem 'capistrano-rbenv' 119 | gem 'capistrano-bundle_rsync' 120 | ``` 121 | 122 | Run `bundle exec cap install`. 123 | 124 | ```bash 125 | $ bundle 126 | $ bundle exec cap install 127 | ``` 128 | 129 | This creates the following files: 130 | 131 | ``` 132 | ├── Capfile 133 | ├── config 134 | │ ├── deploy 135 | │ │ ├── production.rb 136 | │ │ └── staging.rb 137 | │ └── deploy.rb 138 | └── lib 139 | └── capistrano 140 | └── tasks 141 | ``` 142 | 143 | To create different stages: 144 | 145 | ```bash 146 | $ bundle exec cap install STAGES=localhost,sandbox,qa,production 147 | ``` 148 | 149 | Edit Capfile: 150 | 151 | ```ruby 152 | # Load DSL and Setup Up Stages 153 | require 'capistrano/setup' 154 | 155 | # Includes default deployment tasks 156 | require 'capistrano/deploy' 157 | 158 | # Includes tasks from other gems included in your Gemfile 159 | require 'capistrano/rbenv' 160 | 161 | # capistrano-3.3.3 - 3.6.1 162 | require 'capistrano/bundle_rsync' 163 | 164 | # capistrano-3.7+ 165 | require 'capistrano/bundle_rsync/plugin' 166 | install_plugin Capistrano::BundleRsync::Plugin 167 | 168 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 169 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 170 | ``` 171 | 172 | Edit `config/deploy/localhost.rb` as followings for example: 173 | 174 | ```ruby 175 | set :branch, ENV['BRANCH'] || 'master' 176 | set :rbenv_type, :user 177 | set :linked_dirs, %w(log tmp/pids tmp/cache tmp/sockets vendor/bundle tmp/run) 178 | set :keep_releases, 5 179 | set :scm, :bundle_rsync # Need this when run with capistrano-3.3.3 - 3.6.1 180 | set :bundle_rsync_max_parallels, ENV['PARA'] 181 | set :bundle_rsync_rsync_bwlimit, ENV['BWLIMIT'] # like 20000 182 | set :bundle_rsync_config_files, ['~/config/database.yml'] 183 | 184 | set :application, 'sample' 185 | set :repo_url, "git@github.com:sonots/rails-sample.git" 186 | set :deploy_to, "/home/sonots/sample" 187 | set :rbenv_ruby, "2.1.2" # Required on both deploy machine and remote machines 188 | set :ssh_options, user: 'sonots', keys: [File.expand_path('~/.ssh/id_rsa'), File.expand_path('~/.ssh/id_dsa')] 189 | 190 | role :app, ['127.0.0.1'] 191 | ``` 192 | 193 | Deploy by following command: 194 | 195 | ```bash 196 | $ bundle exec cap localhost deploy 197 | ``` 198 | 199 | ## Run a custom task before rsyncing 200 | 201 | For example, if you want to precompile rails assets before rsyncing, 202 | you may add your own task before `bundle_rsync:rsync_release`. 203 | 204 | ```ruby 205 | task :precompile do 206 | config = Capistrano::BundleRsync::Config 207 | run_locally do 208 | Bundler.with_clean_env do 209 | within config.local_release_path do 210 | execute :bundle, 'install' # install development gems 211 | execute :bundle, 'exec rake assets:precompile' 212 | end 213 | end 214 | 215 | hosts = release_roles(:all) 216 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 217 | execute "rsync -az -e ssh #{config.local_release_path}/public/ #{host}:#{fetch(:deploy_to)}/shared/public" 218 | end 219 | end 220 | end 221 | 222 | before "bundle_rsync:rsync_release", "precompile" 223 | ``` 224 | 225 | ## local_git scm 226 | 227 | `bundle_rsync` supports `local_git` SCM strategy in addition to `git`. 228 | 229 | `local_git` strategy enables to rsync the git repository located on a local path without git clone. You may find as this is useful when you need to replace files locally without commit beforehand (for example, for password embedding). 230 | 231 | Following is an example of `config/deploy/xxxx.rb`: 232 | 233 | Please note that `repo_url` should be a different path with the path running cap. 234 | This strategy probably fits with a rare situation, you should use the default `git` strategy usually. 235 | 236 | ```ruby 237 | set :branch, ENV['BRANCH'] || 'master' 238 | set :rbenv_type, :user 239 | set :linked_dirs, %w(log tmp/pids tmp/cache tmp/sockets vendor/bundle tmp/run) 240 | set :keep_releases, 5 241 | set :scm, :bundle_rsync 242 | set :bundle_rsync_scm, 'local_git' # Set `local_git` 243 | 244 | set :application, 'sample' 245 | set :repo_url, "/path/to/app/local" # Need to git clone your repository to this path beforehand. 246 | # This path should be different with the path running cap. 247 | set :deploy_to, "/path/to/app/production" 248 | set :rbenv_ruby, "2.1.2" # Required the same ruby on both deploy machine and remote machines 249 | set :ssh_options, user: 'sonots', keys: File.expand_path('~/.ssh/id_rsa') 250 | 251 | role :app, ['127.0.0.1'] 252 | ``` 253 | 254 | ## FAQ 255 | 256 | Q. What is difference with [capistrano-rsync](https://github.com/moll/capistrano-rsync)? 257 | 258 | A. capistrano-bundle\_rsync does `bundle install` at the deploy machine, not on each remote machine. 259 | 260 | ## ToDo 261 | 262 | 1. Support other SCMs than `git`. 263 | 264 | ## ChangeLog 265 | 266 | [CHANGELOG.md](./CHANGELOG.md) 267 | 268 | ## Contributing 269 | 270 | 1. Fork it 271 | 2. Create your feature branch (`git checkout -b my-new-feature`) 272 | 3. Commit your changes (`git commit -am 'Add some feature'`) 273 | 4. Push to the branch (`git push origin my-new-feature`) 274 | 5. Create new Pull Request 275 | 276 | ## Copyright 277 | 278 | See [LICENSE.txt](./LICENSE.txt) for details. 279 | 280 | ## Special Thanks 281 | 282 | capistrano-bundle_rsync was originally created by [@tohae](https://github.com/tohae). Thank you very much! 283 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /capistrano-bundle_rsync.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "capistrano-bundle_rsync" 7 | spec.version = "0.6.0" 8 | spec.authors = ["Naotoshi Seo", "tohae"] 9 | spec.email = ["sonots@gmail.com", "tohaechan@gmail.com"] 10 | spec.description = %q{Deploy an application and bundled gems via rsync} 11 | spec.summary = %q{Deploy an application and bundled gems via rsync.} 12 | spec.homepage = "https://github.com/sonots/capistrano-bundle_rsync" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files`.split($/) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency 'capistrano', '>= 3.3.3' 21 | spec.add_dependency 'parallel' 22 | spec.add_development_dependency "bundler" 23 | spec.add_development_dependency "rake" 24 | spec.add_development_dependency "rspec", "~> 3" 25 | end 26 | -------------------------------------------------------------------------------- /example/Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and Setup Up Stages 2 | require 'capistrano/setup' 3 | 4 | # Includes default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Includes tasks from other gems included in your Gemfile 8 | require 'capistrano/rbenv' 9 | 10 | if Gem::Version.new(Capistrano::VERSION) < Gem::Version.new('3.7.0') 11 | require 'capistrano/bundle_rsync' 12 | else 13 | require 'capistrano/bundle_rsync/plugin' 14 | install_plugin Capistrano::BundleRsync::Plugin 15 | end 16 | 17 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 18 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 19 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'capistrano' 4 | gem 'capistrano-rbenv' 5 | gem 'capistrano-bundle_rsync', path: '../' 6 | gem 'pry' 7 | gem 'pry-nav' 8 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | How to run: 2 | 3 | ``` 4 | brew install vagrant 5 | vagrant up 6 | ``` 7 | 8 | prepare rbenv, ruby in the vagrant box 9 | 10 | ``` 11 | vagrant ssh 12 | sudo yum install gcc make openssl-devel readline-devel zlib-devel 13 | git clone https://github.com/rbenv/rbenv.git ~/.rbenv 14 | git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build 15 | echo 'export PATH="$HOME/.rbenv/bin:$PATH" && eval "$(rbenv init -)"' >> ~/.bash_profile 16 | export PATH="$HOME/.rbenv/bin:$PATH" && eval "$(rbenv init -)" 17 | rbenv install 2.3.0 18 | ``` 19 | 20 | prepare ssh keys in the vagrant box 21 | 22 | ``` 23 | ssh-keygen 24 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 25 | ``` 26 | 27 | prepare capistrano-bundle_rsync repository in the vagrant box 28 | 29 | ``` 30 | git clone git@github.com:sonots/capistrano-bundle_rsync 31 | cd capistrano-bundle_rsync/example 32 | bundle install --path vendor/bundle 33 | ``` 34 | 35 | run 36 | 37 | ``` 38 | bundle exec cap git deploy 39 | ``` 40 | 41 | ``` 42 | git clone git@github.com:sonots/try_rails4.git 43 | bundle exec cap local_git deploy 44 | ``` 45 | 46 | ``` 47 | bundle exec cap skip_bundle deploy 48 | ``` 49 | -------------------------------------------------------------------------------- /example/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # All Vagrant configuration is done here. The most common configuration 9 | # options are documented and commented below. For a complete reference, 10 | # please see the online documentation at vagrantup.com. 11 | 12 | # Every Vagrant virtual environment requires a box to build off of. 13 | config.vm.box = "centos6.5.3" 14 | config.vm.box_url = 'https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box' 15 | 16 | # name 17 | config.vm.define "vagrant-centos" 18 | config.ssh.forward_agent = true 19 | 20 | config.vm.provider "virtualbox" do |vb| 21 | # Use VBoxManage to customize the VM. For example to change memory: 22 | vb.customize ["modifyvm", :id, "--memory", "1024"] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /example/config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :user, `whoami`.chomp 2 | set :ssh_options, user: ENV['USER'], keys: [File.expand_path('~/.ssh/id_rsa')] 3 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 4 | 5 | set :linked_dirs, %w(log tmp/pids vendor/bundle) 6 | set :keep_releases, 5 7 | set :rbenv_type, :user 8 | set :rbenv_ruby, RUBY_VERSION # '2.1.5' 9 | set :deploy_to, "#{ENV['HOME']}/sample" 10 | 11 | if Gem::Version.new(Capistrano::VERSION) < Gem::Version.new('3.7.0') 12 | set :scm, :bundle_rsync 13 | end 14 | 15 | set :bundle_rsync_max_parallels, ENV['PARA'] 16 | set :bundle_rsync_rsync_bwlimit, ENV['BWLIMIT'] # like 20000 17 | set :bundle_rsync_shared_dirs, File.expand_path('..', __dir__) # rsync example to shared/example 18 | set :bundle_rsync_rsync_options, "-az --delete --exclude=.git" 19 | 20 | namespace :deploy do 21 | desc 'Restart web application' 22 | task :restart do 23 | on roles(:web), in: :sequence, wait: 5 do 24 | # execute some restart code 25 | end 26 | end 27 | 28 | after :finishing, 'deploy:cleanup' 29 | # after :publishing, :restart 30 | end 31 | -------------------------------------------------------------------------------- /example/config/deploy/git.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Capistrano::VERSION) < Gem::Version.new('3.7.0') 2 | set :scm, :bundle_rsync 3 | end 4 | 5 | set :bundle_rsync_scm, 'git' 6 | set :repo_url, 'https://github.com/sonots/try_rails4' 7 | set :branch, 'master' 8 | 9 | role :app, ['127.0.0.1'] 10 | 11 | task :precompile do 12 | config = Capistrano::BundleRsync::Config 13 | run_locally do 14 | Bundler.with_clean_env do 15 | within config.local_release_path do 16 | execute :bundle, 'install' # install development gems 17 | execute :bundle, 'exec rake assets:precompile' 18 | end 19 | end 20 | 21 | hosts = release_roles(:all) 22 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 23 | execute "rsync -az -e ssh #{config.local_release_path}/public/ #{host}:#{fetch(:deploy_to)}/shared/public" 24 | end 25 | end 26 | end 27 | 28 | before "bundle_rsync:rsync_release", "precompile" 29 | -------------------------------------------------------------------------------- /example/config/deploy/local_git.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Capistrano::VERSION) < Gem::Version.new('3.7.0') 2 | set :scm, :bundle_rsync 3 | end 4 | 5 | set :bundle_rsync_scm, 'local_git' 6 | set :repo_url, "#{ENV['PWD']}/try_rails4" # git clone git@github.com:sonots/try_rails4.git 7 | 8 | role :app, ['127.0.0.1'] 9 | -------------------------------------------------------------------------------- /example/config/deploy/skip_bundle.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Capistrano::VERSION) < Gem::Version.new('3.7.0') 2 | set :scm, :bundle_rsync 3 | end 4 | 5 | set :bundle_rsync_scm, 'git' 6 | set :repo_url, 'https://github.com/sonots/try_rails4' 7 | set :branch, 'master' 8 | set :bundle_rsync_skip_bundle, true 9 | 10 | role :app, ['127.0.0.1'] 11 | -------------------------------------------------------------------------------- /lib/capistrano-bundle_rsync.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/capistrano-bundle_rsync/9635277f7af2f5d96133c046539f10ca439129e6/lib/capistrano-bundle_rsync.rb -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module BundleRsync 3 | end 4 | end 5 | 6 | require 'capistrano/bundle_rsync/defaults' 7 | Capistrano::BundleRsync::Defaults.set_defaults 8 | 9 | load File.expand_path('../tasks/bundle_rsync.rake', __FILE__) 10 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/base.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | require 'capistrano/bundle_rsync/config' 3 | 4 | module Capistrano::BundleRsync 5 | class Base < SimpleDelegator 6 | def initialize(delegator) 7 | super(delegator) 8 | end 9 | 10 | def config 11 | Capistrano::BundleRsync::Config 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/bundler.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/bundle_rsync/base' 2 | require 'capistrano/configuration/filter' 3 | 4 | class Capistrano::BundleRsync::Bundler < Capistrano::BundleRsync::Base 5 | def install 6 | within config.local_release_app_path do 7 | Bundler.public_send(Bundler.respond_to?(:with_unbundled_env) ? :with_unbundled_env : :with_clean_env) do 8 | with bundle_app_config: config.local_base_path, rbenv_version: nil, rbenv_dir: nil do 9 | bundle_commands = if test :rbenv, 'version' 10 | %w[rbenv exec bundle] 11 | else 12 | %w[bundle] 13 | end 14 | 15 | execute *bundle_commands, 'config', '--local', 'deployment', 'true' 16 | execute *bundle_commands, 'config', '--local', 'path', config.local_bundle_path 17 | execute *bundle_commands, 'config', '--local', 'without', *config.bundle_without 18 | 19 | opts = ['--quiet'] 20 | if jobs = config.bundle_install_jobs 21 | opts.push('--jobs', jobs) 22 | end 23 | 24 | if standalone = config.bundle_install_standalone_option 25 | opts.push(standalone) 26 | end 27 | 28 | execute *bundle_commands, *opts 29 | execute :rm, "#{config.local_base_path}/config" 30 | end 31 | end 32 | end 33 | end 34 | 35 | def rsync 36 | hosts = release_roles(:all) 37 | release_app_path = config.release_app_path 38 | on hosts, in: :groups, limit: config.max_parallels(hosts) do 39 | within release_app_path do 40 | execute :mkdir, '-p', '.bundle' 41 | end 42 | end 43 | 44 | lines = <<-EOS 45 | --- 46 | BUNDLE_FROZEN: '1' 47 | BUNDLE_PATH: #{shared_path.join('bundle')} 48 | BUNDLE_WITHOUT: #{config.bundle_without.join(':')} 49 | BUNDLE_DISABLE_SHARED_GEMS: '1' 50 | BUNDLE_BIN: #{release_app_path.join('bin')} 51 | EOS 52 | # BUNDLE_BIN requires rbenv-binstubs plugin to make it effectively work 53 | bundle_config_path = "#{config.local_base_path}/bundle_config" 54 | File.open(bundle_config_path, "w") {|file| file.print(lines) } 55 | 56 | hosts = ::Capistrano::Configuration.env.filter(hosts) 57 | rsync_options = config.rsync_options 58 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 59 | ssh = config.build_ssh_command(host) 60 | execute :rsync, "#{rsync_options} --rsh='#{ssh}' #{config.local_bundle_path}/ #{host}:#{shared_path}/bundle/" 61 | execute :rsync, "#{rsync_options} --rsh='#{ssh}' #{bundle_config_path} #{host}:#{release_app_path}/.bundle/config" 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/config.rb: -------------------------------------------------------------------------------- 1 | module Capistrano::BundleRsync 2 | class Config 3 | def self.local_base_path 4 | @local_base_path ||= fetch(:bundle_rsync_local_base_path) 5 | end 6 | 7 | def self.local_mirror_path 8 | @local_mirror_path ||= fetch(:bundle_rsync_local_mirror_path) 9 | end 10 | 11 | def self.local_releases_path 12 | @local_releases_path ||= fetch(:bundle_rsync_local_releases_path) 13 | end 14 | 15 | def self.local_release_path 16 | @local_release_path ||= fetch(:bundle_rsync_local_release_path) 17 | end 18 | 19 | def self.local_release_app_path 20 | @local_release_app_path ||= Pathname.new(local_release_path).join(fetch(:bundle_rsync_app_path)) 21 | end 22 | 23 | def self.local_bundle_path 24 | @local_bundle_path ||= fetch(:bundle_rsync_local_bundle_path) 25 | end 26 | 27 | def self.release_app_path 28 | @release_app_path ||= release_path.join(fetch(:bundle_rsync_app_path)) 29 | end 30 | 31 | def self.config_files 32 | return nil unless config_files = fetch(:bundle_rsync_config_files) 33 | config_files.is_a?(Array) ? config_files : [config_files] 34 | end 35 | 36 | def self.shared_dirs 37 | return nil unless shared_dirs = fetch(:bundle_rsync_shared_dirs) 38 | shared_dirs.is_a?(Array) ? shared_dirs : [shared_dirs] 39 | end 40 | 41 | # Build ssh command options for rsync 42 | # 43 | # First, search available user and keys configurations for each role: 44 | # 45 | # role :app, ['hostname'], { 46 | # user: username, 47 | # keys: File.expand_path('~/.ssh/id_rsa'), 48 | # port: 22, 49 | # } 50 | # 51 | # If not available, look :bundle_rsync_ssh_options: 52 | # 53 | # set :bundle_rsync_ssh_options { 54 | # user: username, 55 | # keys: [File.expand_path('~/.ssh/id_rsa')], 56 | # port: 22, 57 | # } 58 | # 59 | # If :bundle_rsync_ssh_options are not available also, look :ssh_options finally: 60 | # 61 | # set :ssh_options { 62 | # user: username, 63 | # keys: [File.expand_path('~/.ssh/id_rsa')], 64 | # port: 22, 65 | # } 66 | # 67 | # `keys` can be a string or an array. 68 | # NOTE: :password is not supported. 69 | def self.build_ssh_command(host) 70 | user_opt, key_opt, port_opt = "", "", "" 71 | ssh_options = fetch(:bundle_rsync_ssh_options) 72 | if user = host.user || ssh_options[:user] 73 | user_opt = " -l #{user}" 74 | end 75 | if keys = (host.keys.empty? ? ssh_options[:keys] : host.keys) 76 | keys = keys.is_a?(Array) ? keys : [keys] 77 | key_opt = keys.map {|key| " -i #{key}" }.join("") 78 | end 79 | if port = host.port || ssh_options[:port] 80 | port_opt = " -p #{port}" 81 | end 82 | "ssh#{user_opt}#{key_opt}#{port_opt}" 83 | end 84 | 85 | def self.keep_releases 86 | @keep_releases = fetch(:bundle_rsync_keep_releases) 87 | end 88 | 89 | # Fetch the :bundle_rsync_max_parallels, 90 | # where the default is the number of hosts 91 | def self.max_parallels(hosts) 92 | (fetch(:bundle_rsync_max_parallels) || hosts.size).to_i 93 | end 94 | 95 | def self.rsync_options 96 | fetch(:bundle_rsync_rsync_options) 97 | end 98 | 99 | def self.skip_bundle 100 | fetch(:bundle_rsync_skip_bundle) 101 | end 102 | 103 | def self.bundle_install_jobs 104 | fetch(:bundle_rsync_bundle_install_jobs) 105 | end 106 | 107 | def self.bundle_install_standalone 108 | fetch(:bundle_rsync_bundle_install_standalone) 109 | end 110 | 111 | def self.bundle_install_standalone_option 112 | case value = self.bundle_install_standalone 113 | when true 114 | "--standalone" 115 | when Array 116 | "--standalone #{value.join(' ')}" 117 | when String 118 | "--standalone #{value}" 119 | else 120 | nil 121 | end 122 | end 123 | 124 | def self.bundle_without 125 | fetch(:bundle_rsync_bundle_without) 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/defaults.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module BundleRsync 3 | module Defaults 4 | def self.set_defaults 5 | set_if_empty :bundle_rsync_scm, 'git' 6 | 7 | set_if_empty :bundle_rsync_local_base_path, "#{Dir::pwd}/.local_repo" 8 | set_if_empty :bundle_rsync_local_mirror_path, -> { "#{fetch(:bundle_rsync_local_base_path)}/mirror" } 9 | set_if_empty :bundle_rsync_local_releases_path, -> { "#{fetch(:bundle_rsync_local_base_path)}/releases" } 10 | set_if_empty :bundle_rsync_local_release_path, -> { 11 | if fetch(:bundle_rsync_scm).to_s == 'local_git' 12 | repo_url 13 | else # git (default) 14 | "#{fetch(:bundle_rsync_local_releases_path)}/#{Time.new.strftime('%Y%m%d%H%M%S')}" 15 | end 16 | } 17 | set_if_empty :bundle_rsync_local_bundle_path, -> { "#{fetch(:bundle_rsync_local_base_path)}/bundle" } 18 | 19 | set_if_empty :bundle_rsync_ssh_options, -> { fetch(:ssh_options, {}) } 20 | set_if_empty :bundle_rsync_keep_releases, -> { fetch(:keep_releases) } 21 | 22 | set_if_empty :bundle_rsync_max_parallels, -> { release_roles(:all).size } 23 | set_if_empty :bundle_rsync_rsync_bwlimit, nil 24 | set_if_empty :bundle_rsync_rsync_options, -> { 25 | bwlimit = fetch(:bundle_rsync_rsync_bwlimit) 26 | 27 | bwlimit_option = bwlimit ? " --bwlimit #{bwlimit}" : "" 28 | "-az --delete#{bwlimit_option}" 29 | } 30 | 31 | set_if_empty :bundle_rsync_config_files, nil 32 | set_if_empty :bundle_rsync_shared_dirs, nil 33 | set_if_empty :bundle_rsync_app_path, '.' 34 | 35 | set_if_empty :bundle_rsync_skip_bundle, false # NOTE: This is secret option 36 | set_if_empty :bundle_rsync_bundle_install_jobs, nil 37 | set_if_empty :bundle_rsync_bundle_install_standalone, nil 38 | set_if_empty :bundle_rsync_bundle_without, [:development, :test] 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/git.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/bundle_rsync/scm' 2 | require 'capistrano/configuration/filter' 3 | 4 | class Capistrano::BundleRsync::Git < Capistrano::BundleRsync::SCM 5 | def check 6 | execute("git ls-remote #{repo_url} HEAD") 7 | execute("mkdir -p #{config.local_base_path}") 8 | end 9 | 10 | def clone 11 | if File.exist?("#{config.local_mirror_path}/HEAD") 12 | info t(:mirror_exists, at: config.local_mirror_path) 13 | else 14 | execute :git, :clone, '--mirror', repo_url, config.local_mirror_path 15 | end 16 | end 17 | 18 | def update 19 | within config.local_mirror_path do 20 | execute :git, :remote, :update, '--prune' 21 | end 22 | end 23 | 24 | def create_release 25 | execute "mkdir -p #{config.local_release_path}" 26 | 27 | within config.local_mirror_path do 28 | if tree = fetch(:repo_tree) 29 | stripped = tree.slice %r#^/?(.*?)/?$#, 1 # strip both side / 30 | num_components = stripped.count('/') + 1 31 | execute :git, :archive, fetch(:branch), tree, "| tar -x --strip-components #{num_components} -f - -C ", "#{config.local_release_path}" 32 | else 33 | execute :git, :archive, fetch(:branch), '| tar -x -C', "#{config.local_release_path}" 34 | end 35 | end 36 | end 37 | 38 | def clean_release 39 | # Do not remove if :bundle_rsync_local_release_path is directly specified 40 | # because releases/#{datetime} directories are no longer created. 41 | return if fetch(:bundle_rsync_local_release_path) 42 | releases = capture(:ls, '-x', config.local_releases_path).split 43 | if releases.count >= config.keep_releases 44 | directories = (releases - releases.last(config.keep_releases)) 45 | if directories.any? 46 | directories_str = directories.map do |release| 47 | File.join(config.local_releases_path, release) 48 | end.join(" ") 49 | execute :rm, '-rf', directories_str 50 | end 51 | end 52 | end 53 | 54 | def rsync_release 55 | hosts = ::Capistrano::Configuration.env.filter(release_roles(:all)) 56 | rsync_options = config.rsync_options 57 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 58 | ssh = config.build_ssh_command(host) 59 | execute :rsync, "#{rsync_options} --rsh='#{ssh}' #{config.local_release_path}/ #{host}:#{release_path}/" 60 | end 61 | end 62 | 63 | def set_current_revision 64 | within config.local_mirror_path do 65 | set :current_revision, capture(:git, "rev-list --max-count=1 #{fetch(:branch)}") 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/git_turbo.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'capistrano/bundle_rsync/scm' 3 | 4 | # Make Deployment Super-Fast by Utilizing Hardlink and Rsync 5 | class Capistrano::BundleRsync::GitTurbo < Capistrano::BundleRsync::SCM 6 | def check 7 | execute :git, 'ls-remote', repo_url, 'HEAD' 8 | execute :mkdir, '-p', config.local_base_path 9 | execute :mkdir, '-p', config.local_releases_path 10 | end 11 | 12 | def clone 13 | if test "[ -f #{config.local_mirror_path}/HEAD ]" 14 | info t(:mirror_exists, at: config.local_mirror_path) 15 | else 16 | execute :git, :clone, '--mirror', repo_url, config.local_mirror_path 17 | end 18 | end 19 | 20 | def update 21 | within config.local_mirror_path do 22 | execute :git, :remote, :update, '--prune' 23 | end 24 | end 25 | 26 | def create_release 27 | set_current_revision 28 | 29 | worktree = local_worktree_path 30 | if test "[ -f #{worktree.join('.git')} ]" 31 | within worktree do 32 | execute :git, 'checkout', '--detach', fetch(:current_revision) 33 | execute :git, 'reset', '--hard' 34 | execute :git, 'clean', '--force', '-x' 35 | end 36 | else 37 | within config.local_mirror_path do 38 | execute :git, 'worktree', 'add', '-f', '--detach', worktree, fetch(:current_revision) 39 | end 40 | end 41 | 42 | source = fetch(:repo_tree) ? worktree.join(fetch(:repo_tree)) : worktree 43 | within config.local_base_path do 44 | execute :cp, '-al', source, config.local_release_path 45 | end 46 | 47 | within config.local_release_path do 48 | execute :rm, '-f', '.git' 49 | end 50 | end 51 | 52 | def clean_release 53 | releases = capture(:ls, '-x', config.local_releases_path).split 54 | if releases.count >= config.keep_releases 55 | directories = (releases - releases.last(config.keep_releases)) 56 | if directories.any? 57 | directories_str = directories.map do |release| 58 | File.join(config.local_releases_path, release) 59 | end.join(' ') 60 | execute :rm, '-rf', directories_str 61 | end 62 | end 63 | end 64 | 65 | def rsync_release 66 | hosts = release_roles(:all) 67 | c = config 68 | on hosts, in: :groups, limit: config.max_parallels(hosts), wait: 0 do |host| 69 | execute :mkdir, '-p', releases_path 70 | 71 | if source = capture(:ls, '-x', releases_path).split.last 72 | execute :cp, '-al', releases_path.join(source), release_path 73 | else 74 | execute :mkdir, '-p', release_path 75 | end 76 | 77 | run_locally do 78 | ssh = c.build_ssh_command(host) 79 | execute :rsync, "#{c.rsync_options} --rsh='#{ssh}' #{c.local_release_path}/ #{host}:#{release_path}/" 80 | end 81 | end 82 | end 83 | 84 | def set_current_revision 85 | within config.local_mirror_path do 86 | set :current_revision, capture(:git, "rev-list --max-count=1 #{fetch(:branch)}") 87 | end 88 | end 89 | 90 | private 91 | 92 | def local_worktree_path 93 | Pathname.new("#{fetch(:bundle_rsync_local_base_path)}/git_turbo_worktree") 94 | end 95 | end 96 | 97 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/local_git.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/bundle_rsync/scm' 2 | require 'capistrano/configuration/filter' 3 | 4 | class Capistrano::BundleRsync::LocalGit < Capistrano::BundleRsync::SCM 5 | def check 6 | raise ArgumentError.new('`repo_url` must be local path to use `local_git` scm') unless local_path?(repo_url) 7 | execute("git ls-remote #{repo_url} HEAD") 8 | execute("mkdir -p #{config.local_base_path}") 9 | end 10 | 11 | def clone 12 | end 13 | 14 | def update 15 | end 16 | 17 | def create_release 18 | end 19 | 20 | def clean_release 21 | end 22 | 23 | def rsync_release 24 | hosts = ::Capistrano::Configuration.env.filter(release_roles(:all)) 25 | rsync_options = config.rsync_options 26 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 27 | ssh = config.build_ssh_command(host) 28 | if tree = fetch(:repo_tree) 29 | execute :rsync, "#{rsync_options} --rsh='#{ssh}' #{repo_url}/#{tree}/ #{host}:#{release_path}/" 30 | else 31 | execute :rsync, "#{rsync_options} --rsh='#{ssh}' #{repo_url}/ #{host}:#{release_path}/" 32 | end 33 | end 34 | end 35 | 36 | def set_current_revision 37 | within repo_url do 38 | set :current_revision, capture(:git, "rev-parse --short HEAD") 39 | end 40 | end 41 | 42 | private 43 | 44 | def local_path?(repo_url) 45 | return !( 46 | repo_url.start_with?('http://') or 47 | repo_url.start_with?('https://') or 48 | repo_url.start_with?('git://') or 49 | repo_url.start_with?('git@') or 50 | repo_url.start_with?('ssh://') 51 | ) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/plugin.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/bundle_rsync' 2 | require 'capistrano/scm/plugin' 3 | require 'capistrano/bundle_rsync/defaults' 4 | 5 | module Capistrano 6 | module BundleRsync 7 | class Plugin < Capistrano::SCM::Plugin 8 | def set_defaults 9 | Defaults.set_defaults 10 | end 11 | 12 | def register_hooks 13 | after "deploy:new_release_path", "bundle_rsync:create_release" 14 | before "deploy:check", "bundle_rsync:check" 15 | before "deploy:set_current_revision", "bundle_rsync:set_current_revision" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/capistrano/bundle_rsync/scm.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/bundle_rsync/base' 2 | require 'capistrano/configuration/filter' 3 | 4 | # Base class for SCM strategy providers. 5 | # 6 | # @abstract 7 | class Capistrano::BundleRsync::SCM < Capistrano::BundleRsync::Base 8 | # @abstract 9 | # 10 | # Your implementation should check if the specified remote-repository is 11 | # available. 12 | # 13 | # @return [Boolean] 14 | # 15 | def check 16 | raise NotImplementedError.new( 17 | "Your SCM strategy module should provide a #check method" 18 | ) 19 | end 20 | 21 | # @abstract 22 | # 23 | # Create a (new) clone of the remote-repository on the deployment target 24 | # 25 | # @return void 26 | # 27 | def clone 28 | raise NotImplementedError.new( 29 | "Your SCM strategy module should provide a #clone method" 30 | ) 31 | end 32 | 33 | # @abstract 34 | # 35 | # Update the clone on the deployment target 36 | # 37 | # @return void 38 | # 39 | def update 40 | raise NotImplementedError.new( 41 | "Your SCM strategy module should provide a #update method" 42 | ) 43 | end 44 | 45 | # @abstract 46 | # 47 | # Copy the contents of the cache-repository onto the release path 48 | # 49 | # @return void 50 | # 51 | def create_release 52 | raise NotImplementedError.new( 53 | "Your SCM strategy module should provide a #create_release method" 54 | ) 55 | end 56 | 57 | # @abstract 58 | # 59 | # Clean the contents of the cache-repository onto the release path 60 | # 61 | # @return void 62 | # 63 | def clean_release 64 | raise NotImplementedError.new( 65 | "Your SCM strategy module should provide a #clean_release method" 66 | ) 67 | end 68 | 69 | # @abstract 70 | # 71 | # Rsync the contents of the release path 72 | # 73 | # This is an additional task endpoint provided by capistrano-bundle_rsync 74 | # 75 | # @return void 76 | # 77 | def rsync_release 78 | raise NotImplementedError.new( 79 | "Your SCM strategy module should provide a #rsync_release method" 80 | ) 81 | end 82 | 83 | # @abstract 84 | # 85 | # Rsync arbitrary contents to shared directory 86 | # 87 | # This is an additional task endpoint provided by capistrano-bundle_rsync 88 | # 89 | # @return void 90 | def rsync_shared 91 | hosts = ::Capistrano::Configuration.env.filter(release_roles(:all)) 92 | rsync_options = config.rsync_options 93 | 94 | if config_files = config.config_files 95 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 96 | ssh = config.build_ssh_command(host) 97 | execute :rsync, rsync_options, "--rsh='#{ssh}'", *config_files.map(&:to_s), "#{host}:#{config.release_app_path}/config/" 98 | end 99 | end 100 | 101 | if shared_dirs = config.shared_dirs 102 | Parallel.each(hosts, in_threads: config.max_parallels(hosts)) do |host| 103 | ssh = config.build_ssh_command(host) 104 | shared_dirs_without_suffix = shared_dirs.map { |dir| dir.to_s.delete_suffix('/') } 105 | execute :rsync, rsync_options, "--rsh='#{ssh}'", *shared_dirs_without_suffix, "#{host}:#{shared_path}/" 106 | end 107 | end 108 | end 109 | 110 | # @abstract 111 | # 112 | # Identify the SHA of the commit that will be deployed. This will most likely involve SshKit's capture method. 113 | # 114 | # @return void 115 | # 116 | def set_current_revision 117 | raise NotImplementedError.new( 118 | "Your SCM strategy module should provide a #set_current_revision method" 119 | ) 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/bundle_rsync.rake: -------------------------------------------------------------------------------- 1 | unless defined?(Capistrano::BundleRsync::TASK_LOADED) # protect multiple loads 2 | Capistrano::BundleRsync::TASK_LOADED = true 3 | require 'fileutils' 4 | require 'parallel' 5 | require 'capistrano/bundle_rsync/bundler' 6 | 7 | namespace :bundle_rsync do 8 | def bundle_rsync_config 9 | Capistrano::BundleRsync::Config 10 | end 11 | 12 | def bundle_rsync_bundler 13 | @bundle_rsync_bundler ||= Capistrano::BundleRsync::Bundler.new(self) 14 | end 15 | 16 | def bundle_rsync_scm 17 | @bundle_rsync_scm ||= 18 | case fetch(:bundle_rsync_scm).to_s 19 | when 'local_git' 20 | require 'capistrano/bundle_rsync/local_git' 21 | set :bundle_rsync_local_release_path, repo_url 22 | Capistrano::BundleRsync::LocalGit.new(self) 23 | when 'git_turbo' 24 | require 'capistrano/bundle_rsync/git_turbo' 25 | Capistrano::BundleRsync::GitTurbo.new(self) 26 | else 27 | require 'capistrano/bundle_rsync/git' 28 | Capistrano::BundleRsync::Git.new(self) 29 | end 30 | end 31 | 32 | namespace :bundler do 33 | task :install do 34 | run_locally do 35 | if bundle_rsync_config.skip_bundle 36 | info "Skip bundle" 37 | else 38 | bundle_rsync_bundler.install 39 | end 40 | end 41 | end 42 | 43 | task :rsync do 44 | run_locally do 45 | if bundle_rsync_config.skip_bundle 46 | info "Skip bundle rsync" 47 | else 48 | bundle_rsync_bundler.rsync 49 | end 50 | end 51 | end 52 | end 53 | 54 | desc 'Check that the repository is reachable' 55 | task :check do 56 | run_locally do 57 | bundle_rsync_scm.check 58 | end 59 | end 60 | 61 | desc 'Clone the repo to the cache' 62 | task :clone do 63 | run_locally do 64 | bundle_rsync_scm.clone 65 | end 66 | end 67 | 68 | desc 'Update the repo mirror to reflect the origin state' 69 | task update: :'bundle_rsync:clone' do 70 | run_locally do 71 | bundle_rsync_scm.update 72 | end 73 | end 74 | 75 | desc 'Copy repo to releases' 76 | task create_release: :'bundle_rsync:update' do 77 | run_locally do 78 | bundle_rsync_scm.create_release 79 | end 80 | end 81 | 82 | # additional extra tasks of bundle_rsync scm 83 | desc 'Rsync releases' 84 | task :rsync_release do 85 | run_locally do 86 | bundle_rsync_scm.rsync_release 87 | end 88 | end 89 | 90 | # additional extra tasks of bundle_rsync scm 91 | desc 'Rsync shared' 92 | task :rsync_shared do 93 | run_locally do 94 | bundle_rsync_scm.rsync_shared 95 | end 96 | end 97 | 98 | # additional extra tasks of bundle_rsync scm 99 | desc 'Clean releases' 100 | task :clean_release do 101 | run_locally do 102 | bundle_rsync_scm.clean_release 103 | end 104 | end 105 | 106 | desc 'Determine the revision that will be deployed' 107 | task :set_current_revision do 108 | run_locally do 109 | bundle_rsync_scm.set_current_revision 110 | end 111 | end 112 | 113 | after 'bundle_rsync:create_release', 'bundle_rsync:bundler:install' 114 | after 'bundle_rsync:bundler:install', 'bundle_rsync:rsync_release' 115 | after 'bundle_rsync:rsync_release', 'bundle_rsync:rsync_shared' 116 | after 'bundle_rsync:rsync_shared', 'bundle_rsync:bundler:rsync' 117 | after 'bundle_rsync:bundler:rsync', 'bundle_rsync:clean_release' 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/capistrano/bundle_rsync/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capistrano::BundleRsync::Config do 4 | describe '.bundle_install_standalone_option' do 5 | before { 6 | allow(described_class).to receive(:fetch).with(:bundle_rsync_bundle_install_standalone).and_return(value) 7 | } 8 | subject { described_class.bundle_install_standalone_option } 9 | context "`set :bundle_rsync_bundle_install_standalone, nil` or the case that it does not be configured" do 10 | let(:value) { nil } 11 | it { should eq nil } 12 | end 13 | context "`set :bundle_rsync_bundle_install_standalone, true`" do 14 | let(:value) { true } 15 | it { should eq "--standalone" } 16 | end 17 | context "`set :bundle_rsync_bundle_install_standalone, false`" do 18 | let(:value) { false } 19 | it { should eq nil } 20 | end 21 | context "`set :bundle_rsync_bundle_install_standalone, ['foo', 'bar']" do 22 | let(:value) { %w(foo bar) } 23 | it { should eq "--standalone foo bar" } 24 | end 25 | context "`set :bundle_rsync_bundle_install_standalone, [:foo, :bar]" do 26 | let(:value) { [:foo, :bar] } 27 | it { should eq "--standalone foo bar" } 28 | end 29 | context "`set :bundle_rsync_bundle_install_standalone, 'foo bar'" do 30 | let(:value) { 'foo bar' } 31 | it { should eq "--standalone foo bar" } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | # Emulate cap command 4 | # https://github.com/capistrano/capistrano/blob/31e142d56f8d894f28404fb225dcdbe7539bda18/bin/cap 5 | require "capistrano/all" 6 | 7 | # Emulate Capfile 8 | 9 | # Load DSL and Setup Up Stages 10 | require 'capistrano/setup' 11 | 12 | # Includes default deployment tasks 13 | require 'capistrano/deploy' 14 | 15 | if Gem::Version.new(Capistrano::VERSION) >= Gem::Version.new('3.7') 16 | require 'capistrano/bundle_rsync/plugin' 17 | install_plugin Capistrano::BundleRsync::Plugin 18 | else 19 | require 'capistrano/bundle_rsync' 20 | end 21 | --------------------------------------------------------------------------------